Compare commits

..

91 Commits

Author SHA1 Message Date
louiscklaw
5640d4d4f6 update, 2025-05-08 17:19:43 +08:00
louiscklaw
7105bc85e3 update, 2025-05-08 14:38:34 +08:00
louiscklaw
61c10821b6 update, 2025-05-08 14:38:30 +08:00
louiscklaw
ea3f99654d update users, 2025-05-08 14:38:18 +08:00
louiscklaw
30f494fc35 update teachers, 2025-05-08 14:37:48 +08:00
louiscklaw
367e58a8cf update working on cms users page, 2025-05-06 17:45:25 +08:00
louiscklaw
99274b9c73 update fix typo, 2025-05-06 08:43:57 +08:00
louiscklaw
51935d203a update, 2025-04-29 22:55:21 +08:00
louiscklaw
9d3e832081 update, 2025-04-29 16:09:30 +08:00
louiscklaw
4c72861eda update, 2025-04-28 08:03:21 +08:00
louiscklaw
d0ea7e5452 init commit, 2025-04-26 10:08:01 +08:00
louiscklaw
7d70b5826b update, 2025-04-26 09:56:29 +08:00
louiscklaw
a00d1ee7ce update, 2025-04-26 09:48:37 +08:00
louiscklaw
7296a10ec1 update, 2025-04-26 09:42:38 +08:00
louiscklaw
957df690f4 update, 2025-04-26 09:40:10 +08:00
louiscklaw
45d5c23512 update, 2025-04-26 07:46:09 +08:00
louiscklaw
caa224cbb6 update, 2025-04-26 07:44:55 +08:00
louiscklaw
9be92b41d1 update, 2025-04-26 07:14:53 +08:00
louiscklaw
6e8fea3bdd update, 2025-04-26 06:15:18 +08:00
louiscklaw
df87cfb037 update, 2025-04-24 23:41:39 +08:00
louiscklaw
d73e5f9c22 update left menu path, 2025-04-24 23:31:33 +08:00
louiscklaw
6884f1466f update vscode plugins, 2025-04-24 20:06:20 +08:00
louiscklaw
d308131a8a update hook hellowlrld sample, 2025-04-24 20:06:09 +08:00
louiscklaw
b3ebe8309a update prompts, 2025-04-24 20:05:54 +08:00
louiscklaw
fa35ef2bef update constants, 2025-04-24 20:04:34 +08:00
louiscklaw
0785fcd144 update notifications, 2025-04-24 20:03:26 +08:00
louiscklaw
2dcc765072 update notifications, 2025-04-24 20:02:56 +08:00
louiscklaw
b8e8968866 update build scripts and prompt,s 2025-04-24 13:19:51 +08:00
louiscklaw
92040c6efb update student pages, 2025-04-24 13:19:21 +08:00
louiscklaw
a3d2ee57f7 update init student, 2025-04-24 12:40:42 +08:00
louiscklaw
90835a7fe3 Merge branch 'develop/cms/frontend/dashboard/students/trunk' into develop/cms/frontend/dashboard/teachers/trunk 2025-04-24 12:10:09 +08:00
louiscklaw
ca2a9c235b update init teacher , 2025-04-24 12:09:47 +08:00
louiscklaw
29b074f6dd init helloworld test, 2025-04-24 10:26:30 +08:00
louiscklaw
2f8acbbcdf init test and snapshot, 2025-04-24 10:25:31 +08:00
louiscklaw
41a35b487a init students, 2025-04-24 02:12:27 +08:00
louiscklaw
7a33549a79 init teachers, 2025-04-24 02:12:07 +08:00
louiscklaw
d81b3e9a9e update summary, 2025-04-24 02:06:05 +08:00
louiscklaw
da08798b10 update, 2025-04-24 01:08:44 +08:00
louiscklaw
41cc82d54d update customers, 2025-04-23 02:02:47 +08:00
louiscklaw
c8d184465a update customer edit form in the middle, 2025-04-23 01:01:31 +08:00
louiscklaw
adc04a1a40 update fix status filter for customer, 2025-04-22 23:16:10 +08:00
louiscklaw
62da367589 update prompt and db schema, 2025-04-22 23:03:52 +08:00
louiscklaw
04ac1a8881 update ai prompt, 2025-04-22 23:03:15 +08:00
louiscklaw
dfd6ecc744 update customers in the middle, 2025-04-22 22:56:30 +08:00
louiscklaw
69cb0718be update customer driver, 2025-04-22 22:55:21 +08:00
louiscklaw
7dc7716f18 update for customers, 2025-04-22 22:28:40 +08:00
louiscklaw
633a1d3a4c update ai prompt, 2025-04-22 18:53:41 +08:00
louiscklaw
64b5f89fdf update build ok, 2025-04-22 18:53:17 +08:00
louiscklaw
f87dd2c3b1 update, in the middle, 2025-04-22 18:12:10 +08:00
louiscklaw
8b3dfe69e4 update schemas.json and seeding, 2025-04-22 17:14:27 +08:00
louiscklaw
375b0a3593 update init cr, no modification done yet, 2025-04-22 15:13:33 +08:00
louiscklaw
bd907b4dde init paths for cr, 2025-04-22 15:12:12 +08:00
louiscklaw
73a2b2dfb9 update, 2025-04-22 13:30:02 +08:00
louiscklaw
2f28d71060 update page.tsx visual=true filter, 2025-04-22 13:19:12 +08:00
louiscklaw
01ce517629 update fix import path, 2025-04-22 12:29:56 +08:00
louiscklaw
033ca95dfe refactor to dedicated directory, 2025-04-22 12:20:21 +08:00
louiscklaw
0fa277c50e update schema and seed, 2025-04-22 12:13:42 +08:00
louiscklaw
e8ded0cb30 update for filter, 2025-04-22 11:57:24 +08:00
louiscklaw
cc6e40c9d0 update delete lp_categories and lp_questions, 2025-04-22 11:55:18 +08:00
louiscklaw
407d851b92 update mf build ok, 2025-04-22 04:40:01 +08:00
louiscklaw
0b38de74a2 update refactoring, 2025-04-22 04:18:53 +08:00
louiscklaw
1c3dccd68e update pocketbase seeding, 2025-04-22 02:53:49 +08:00
louiscklaw
26ef20ffd7 update ticket, 2025-04-22 02:52:43 +08:00
louiscklaw
f9c038fcad update build script, 2025-04-22 02:49:57 +08:00
louiscklaw
aabf729dbf update for testing build, 2025-04-22 02:47:56 +08:00
louiscklaw
374e3de59b update build ok, 2025-04-22 02:05:08 +08:00
louiscklaw
f4f4a0eb7c update tsconfig, 2025-04-22 01:07:34 +08:00
louiscklaw
7f3f02463f update, 2025-04-22 01:07:18 +08:00
louiscklaw
e4aee3f02f fix building *.del directory, it is now not building, 2025-04-22 00:30:21 +08:00
louiscklaw
00a978e55a update, 2025-04-21 12:15:25 +08:00
louiscklaw
ac7d3883fd update, 2025-04-21 07:21:28 +08:00
louiscklaw
63ffbaacd6 update, 2025-04-21 07:20:59 +08:00
louiscklaw
954a42aaa7 update, 2025-04-21 07:09:46 +08:00
louiscklaw
8082afd711 update, 2025-04-21 06:39:53 +08:00
louiscklaw
3e73668a3f update, 2025-04-21 06:39:13 +08:00
louiscklaw
72bc7a67e2 update to mf_categories, 2025-04-21 05:56:09 +08:00
louiscklaw
d2f9472743 update , 2025-04-21 05:36:41 +08:00
louiscklaw
f65f6df660 update for lp_categories, 2025-04-21 05:16:30 +08:00
louiscklaw
3679924a6a update config, 2025-04-21 03:40:36 +08:00
louiscklaw
5a746b3c3a update, 2025-04-20 05:50:05 +08:00
louiscklaw
ba1c0f6897 update listening practice category listing, 2025-04-20 03:24:24 +08:00
louiscklaw
1349605d6e update listening practice category listing, 2025-04-20 03:24:18 +08:00
louiscklaw
790ec73e50 update and bulid ok, 2025-04-20 03:23:04 +08:00
louiscklaw
0f90e0ae72 update build config, 2025-04-20 02:00:47 +08:00
louiscklaw
b963a85cc6 update build ok, 2025-04-20 02:00:25 +08:00
louiscklaw
6b19656833 update translation, 2025-04-19 06:43:30 +08:00
louiscklaw
700a32f7a5 update project config, 2025-04-19 06:43:09 +08:00
louiscklaw
1c865595bf update init lp_category, 2025-04-19 06:42:55 +08:00
louiscklaw
2c20496a13 update seed, 2025-04-19 04:24:50 +08:00
louiscklaw
0d554e70ee update translations, 2025-04-19 03:58:46 +08:00
louiscklaw
ba2138275b update, 2025-04-19 03:25:12 +08:00
1141 changed files with 135493 additions and 4590 deletions

View File

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

View File

@@ -1,137 +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
image file // changed from varchar to file
sound file // changed from varchar to file
word text // changed from varchar to text
word_c text // changed from varchar to text
sample_e text // changed from varchar to text
sample_c text // changed from varchar to text
cat_id text [ref: > LessonCategories.id] // changed type and reference
category text // changed from varchar to text
lesson_type_id text [ref: > LessonTypes.id] // changed type and reference
}
// Listening Practice Quiz Categories
// store listening practice category, (LpCategories, LpCategory)
Table QuizLPCategories {
// system fields
id text [pk]
created datetime [default: `now()`]
updated datetime
// value fields
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 text [pk] // changed from int to text
created datetime [default: `now()`]
updated datetime
word text // changed from varchar to text
sound file // changed from blob to file
cat_id text [ref: > QuizLPCategories.id] // changed type and reference
cat_name text // new field
cat_image file // new field
pos number // new field
init_answer json // new field
visible text // new field
slug text // new field
remarks text // new field
description text // new field
}
// Matching Frenzy Quiz Categories
Table QuizMFCategories {
id text [pk] // changed from int to text
created datetime [default: `now()`]
updated datetime
cat_name text // changed from varchar to text
cat_image file // changed from blob to file
pos number // changed from integer to number
init_answer json
visible text // new field
}
// Matching Frenzy Quiz Questions
Table QuizMFQuestions {
id text [pk] // changed from int to text
created datetime [default: `now()`]
updated datetime
word text // changed from varchar to text
word_c text // changed from varchar to text
cat_id text [ref: > QuizMFCategories.id] // changed type and reference
visible text // new field
sound file // new field
cat_image file // new field
}
// Connectives Revision Quiz Categories
Table QuizCRCategories {
id text [pk] // changed from int to text
created datetime [default: `now()`]
updated datetime
cat_name text // changed from varchar to text
cat_image file // changed from blob to file
pos integer
init_answer json
}
// Connectives Revision Quiz Questions
Table QuizCRQuestions {
id text [pk] // changed from int to text
created datetime [default: `now()`]
updated datetime
question_fh text // changed from varchar to text
question_sh text // changed from varchar to text
modal_ans text // changed from varchar to text
cat_id text [ref: > QuizCRCategories.id] // changed type and reference
options json // new field
}
// Test table
Table t1 {
id text [pk] // changed from int to text
created datetime [default: `now()`]
updated datetime
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
}

View 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
}

View File

@@ -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',
},
},
],
};

View File

@@ -1,3 +1,8 @@
**/*del
**/*bak
**/*log
**/*tmp
.env
.env.production
@@ -12,6 +17,7 @@
**/*.log
**/*.tmp
**/*.del
**/*.plan
**/_archive
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

33
002_source/cms/.vscode/extensions.json vendored Normal file
View 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"
]
}

View File

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

View File

@@ -1,11 +0,0 @@
# guideline
- please divide the problem into small parts
- if you found you cannot understand the problem, please ask the question
- if you found you cannot solve the problem, plesae stop and shout
- review the whole solution before you reply to user
- example for page can be found in `./src/app/_helloworld/page.tsx`
- example for component can be found in `./src/components/_helloworld/index.tsx`
Thanks.

View File

View File

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

View File

@@ -2,7 +2,12 @@
const config = {
reactStrictMode: false,
images: {
domains: ['example.com', '127.0.0.1', 'localhost'],
domains: [
//
'example.com',
'127.0.0.1',
'localhost',
],
},
};

View File

@@ -4,16 +4,16 @@
"version": "7.0.0",
"private": true,
"engines": {
"node": ">=18"
"node": "==22"
},
"scripts": {
"dev": "next dev",
"build": "rm -rf .next && next build",
"build": "next build",
"build:w": "pnpx nodemon --ext ts,tsx,json,mjs,js,jsx --delay 15 --exec \"pnpm run build\"",
"start": "next start",
"lint": "next lint --quiet",
"lint:fix": "next lint --fix",
"lint:w": "pnpx nodemon --ext ts,tsx,json,mjs,js,jsx --delay 60 --exec \"pnpm run lint\"",
"lint:w": "pnpx nodemon --ext ts,tsx,json,mjs,js,jsx --delay 5 --exec \"pnpm run lint\"",
"typecheck": "tsc --noEmit",
"typecheck:w": "tsc --noEmit -w",
"format:write": "prettier --write \"**/*.{js,jsx,mjs,ts,tsx,mdx}\" --cache",
@@ -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",
@@ -116,4 +117,4 @@
"protobufjs"
]
}
}
}

View File

@@ -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)':

View File

@@ -28,7 +28,17 @@ const config = {
'',
'^[./]',
],
plugins: ['@ianvs/prettier-plugin-sort-imports'],
plugins: [
// '@ianvs/prettier-plugin-sort-imports'
],
overrides: [
{
files: ['*.tsx'],
options: {
singleAttributePerLine: true,
},
},
],
};
export default config;

View File

@@ -0,0 +1,3 @@
{
"back-to-list": "Teachers"
}

View File

@@ -1,3 +1,6 @@
{
"languageChanged": "Sprache geändert"
"languageChanged": "Sprache geändert",
"teachers": {
"back-to-list": "Lehrer"
}
}

View File

@@ -1,22 +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`

View File

@@ -1,134 +1,4 @@
{
"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": "名稱為必填"
}
"hello": "world",
"teachers": "teachers"
}

View File

@@ -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 ?": "確定要刪除課程類型嗎?"
}

View File

@@ -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 ?": "確定要刪除課程類型嗎?"
}

View File

@@ -0,0 +1,3 @@
{
"back-to-list": "Teachers"
}

View File

@@ -1,195 +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": "課程分類",
"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": "詳細錯誤資訊"
}

View File

@@ -0,0 +1,3 @@
{
"back-to-list": "Teachers"
}

View File

@@ -1,3 +1,7 @@
{
"languageChanged": "Language changed"
"languageChanged": "Language changed",
"teachers": {
"nav-title": "teachers",
"back-to-list": "Teachers"
}
}

View File

@@ -0,0 +1,3 @@
{
"back-to-list": "Teachers"
}

View File

@@ -1,3 +1,6 @@
{
"languageChanged": "Idioma cambiado"
"languageChanged": "Idioma cambiado",
"teachers": {
"back-to-list": "Profesores"
}
}

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -ex
rm -rf tsconfig.tsbuildinfo
reset
pnpm run typecheck:w

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -ex
reset
rm -rf .next
pnpm run build

View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -ex
pnpm run dev

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -ex
node ./scripts/update_repomix.js
echo "done"

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -ex
node ./scripts/update_repomix.js
echo "done"

View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -ex
git add src
git commit -m'update,'
echo "done"

View File

@@ -1,5 +0,0 @@
#!/usr/bin/env bash
set -ex
npx nodemon --ext tsx,ts --exec "reset && pnpm run build"

View File

@@ -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"

View 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);
});

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

View 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();
});
});

View 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();
});

View File

@@ -0,0 +1,23 @@
'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',
},
];

View File

@@ -0,0 +1,57 @@
'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>
);
}

View File

@@ -0,0 +1,105 @@
'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>
);
}

View File

@@ -0,0 +1,14 @@
'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;

View File

@@ -0,0 +1,20 @@
'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(),
},
];

View File

@@ -0,0 +1,6 @@
export interface Notification {
id: string;
type: string;
status: 'delivered' | 'pending' | 'failed';
createdAt: Date;
}

View File

@@ -0,0 +1,43 @@
'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(),
},
];

View File

@@ -0,0 +1,80 @@
'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>
</>
);
}

View File

@@ -0,0 +1,47 @@
'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>
);
}

View File

@@ -0,0 +1,66 @@
'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 diff suppressed because it is too large Load Diff

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

View 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>
);
}

View File

@@ -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>
);
}

View File

@@ -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[];

View 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;
};
}

View File

@@ -45,13 +45,14 @@ import { Notifications } from '@/components/dashboard/lesson_category/notificati
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 type { LessonCategory } from '@/components/dashboard/lesson_category/types';
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 default function Page(): React.JSX.Element {
const { t } = useTranslation(['common']);
const { t } = useTranslation();
const router = useRouter();
//
const { cat_id: catId } = useParams<{ cat_id: string }>();

View File

@@ -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>
);
}

View File

@@ -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,

View File

@@ -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>
);
}

View 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;
//
};
}

View File

@@ -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[];

View File

@@ -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>
);
}

View File

@@ -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>
</>
);
}

View File

@@ -0,0 +1,135 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import SampleAddressCard from '@/app/dashboard/Sample/AddressCard';
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard';
import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard';
import { COL_QUIZ_CR_CATEGORIES } from '@/constants';
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
import Grid from '@mui/material/Unstable_Grid2';
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
import type { RecordModel } from 'pocketbase';
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
import { toast } from '@/components/core/toaster';
import ErrorDisplay from '@/components/dashboard/error';
import { defaultCrCategory } from '@/components/dashboard/cr/categories/_constants.ts';
import { Notifications } from '@/components/dashboard/cr/categories/notifications';
import type { CrCategory } from '@/components/dashboard/cr/categories/type';
import FormLoading from '@/components/loading';
import BasicDetailCard from './BasicDetailCard';
import TitleCard from './TitleCard';
export default function Page(): React.JSX.Element {
const { t } = useTranslation();
const router = useRouter();
//
const { cat_id: catId } = useParams<{ cat_id: string }>();
//
const [showLoading, setShowLoading] = React.useState<boolean>(true);
const [showError, setShowError] = React.useState({ show: false, detail: '' });
//
const [showLessonCategory, setShowLessonCategory] = React.useState<CrCategory>(defaultCrCategory);
function handleEditClick() {
router.push(paths.dashboard.cr_categories.edit(showLessonCategory.id));
}
React.useEffect(() => {
if (catId) {
pb.collection(COL_QUIZ_CR_CATEGORIES)
.getOne(catId)
.then((model: RecordModel) => {
setShowLessonCategory({ ...defaultCrCategory, ...model });
})
.catch((err) => {
logger.error(err);
toast(t('list.error'));
setShowError({ show: true, detail: JSON.stringify(err) });
})
.finally(() => {
setShowLoading(false);
});
}
}, [catId]);
if (showLoading) return <FormLoading />;
if (showError.show)
return (
<ErrorDisplay
message={t('error.unable-to-process-request')}
code="500"
details={showError.detail}
/>
);
return (
<Box
sx={{
maxWidth: 'var(--Content-maxWidth)',
m: 'var(--Content-margin)',
p: 'var(--Content-padding)',
width: 'var(--Content-width)',
}}
>
<Stack spacing={4}>
<Stack spacing={3}>
<div>
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.cr_categories.list}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
{t('list.title')}
</Link>
</div>
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={3}
sx={{ alignItems: 'flex-start' }}
>
<TitleCard lpModel={showLessonCategory} />
</Stack>
</Stack>
<Grid
container
spacing={4}
>
<Grid
lg={4}
xs={12}
>
<Stack spacing={4}>
<BasicDetailCard
lpModel={showLessonCategory}
handleEditClick={handleEditClick}
/>
<SampleSecurityCard />
</Stack>
</Grid>
<Grid
lg={8}
xs={12}
>
<Stack spacing={4}>
<SamplePaymentCard />
<SampleAddressCard />
<Notifications notifications={SampleNotifications} />
</Stack>
</Grid>
</Grid>
</Stack>
</Box>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
</>
);
}

View 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>
);
}

View File

@@ -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[];

View File

@@ -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>
);
}

View File

@@ -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,

View File

@@ -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>
);
}

View 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;
//
};
}

View File

@@ -0,0 +1,157 @@
// src/app/dashboard/customers/page.tsx
'use client';
import type { Customer } from '@/components/dashboard/customer/type.d';
import { dayjs } from '@/lib/dayjs';
export const SampleCustomers = [
{
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[];

View File

@@ -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>
);
}

View File

@@ -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>
</>
);
}

View File

@@ -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
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}>
<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>
<BasicDetailCard
lpModel={showLessonCategory}
handleEditClick={handleEditClick}
/>
<SampleSecurityCard />
</Stack>
</Grid>
<Grid lg={8} xs={12}>
<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(),
},
]}
/>
<SamplePaymentCard />
<SampleAddressCard />
<Notifications notifications={SampleNotifications} />
</Stack>
</Grid>
</Grid>

View 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 };
}
```

View File

@@ -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,

View File

@@ -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>
);
}

View File

@@ -1,5 +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';
@@ -7,180 +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(() => {
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
@@ -192,28 +161,56 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
}}
>
<Stack spacing={4}>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={3}
sx={{ alignItems: 'flex-start' }}
>
<Box sx={{ flex: '1 1 auto' }}>
<Typography variant="h4">Customers</Typography>
<Typography variant="h4">{t('list.title')}</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button startIcon={<PlusIcon />} variant="contained">
Add
</Button>
<LoadingButton
loading={isLoadingAddPage}
onClick={(): void => {
setIsLoadingAddPage(true);
router.push(paths.dashboard.customers.create);
}}
startIcon={<PlusIcon />}
variant="contained"
>
{t('list.add')}
</LoadingButton>
</Box>
</Stack>
<CustomersSelectionProvider customers={filteredCustomers}>
<CustomersSelectionProvider customers={f}>
<Card>
<CustomersFilters filters={{ email, phone, status }} sortDir={sortDir} />
<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} />
<CustomersPagination
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>
);
}
@@ -253,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 };
}

View File

@@ -12,14 +12,8 @@ import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { LessonCategoryEditForm } from '@/components/dashboard/lesson_category/lesson-category-edit-form';
// import { LessonCategoryEditForm } from '@/components/dashboard/lesson_category/lesson-category-edit-form';
export default function Page(): React.JSX.Element {
const { t } = useTranslation(['common', 'lesson_category']);
React.useEffect(() => {
console.log('helloworld');
}, []);
const { t } = useTranslation(['lesson_category']);
return (
<Box

View File

@@ -1,5 +1,7 @@
import { dayjs } from '@/lib/dayjs';
import { LessonCategory } from '@/components/dashboard/lesson_category/types';
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';

View 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;
};
}

View File

@@ -1,190 +0,0 @@
'use client';
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 { 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/types';
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<boolean>(false);
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
//
const [f, setF] = React.useState<LessonCategory[]>([]);
const [currentPage, setCurrentPage] = React.useState<number>(1);
//
const [recordCount, setRecordCount] = React.useState<number>(0);
const sortedLessonCategories = applySort(lessonCategoriesData, sortDir);
const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status: status });
//
const reloadRows = () => {
setShowLoading(true);
pb.collection(COL_LESSON_CATEGORIES)
.getList(currentPage, rowsPerPage, {})
.then((lessonCategories: ListResult<RecordModel>) => {
// console.log(lessonTypes);
const { items, page, perPage, totalItems, totalPages } = lessonCategories;
const tempLessonCategories: LessonCategory[] = items.map((item) => {
return { ...defaultLessonCategory, ...item };
});
setLessonCategoriesData(tempLessonCategories);
setRecordCount(totalItems);
})
.catch((err) => {
logger.error(err);
toast(t('dashboard.lessonTypes.list.error'));
setShowError(true);
})
.finally(() => {
setShowLoading(false);
});
};
React.useEffect(() => {
reloadRows();
}, []);
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 <pre>{JSON.stringify(lessonCategoriesData, null, 2)}</pre>;
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"
>
{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={filteredLessonCategories} />
</Box>
<Divider />
<LessonCategoriesPagination count={filteredLessonCategories.length + 100} page={0} />
</Card>
</LessonCategoriesSelectionProvider>
</Stack>
</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 }: 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;
}
}
return true;
});
}
interface PageProps {
searchParams: {
email?: string;
phone?: string;
sortDir?: 'asc' | 'desc';
status?: string;
name?: string;
visible?: string;
type?: string;
//
};
}

View File

@@ -1,8 +1,10 @@
'use client';
import * as React from 'react';
// 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';
@@ -26,7 +28,7 @@ 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 type { RecordModel } from 'pocketbase';
import { RecordModel } from 'pocketbase';
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
@@ -37,38 +39,39 @@ import { PropertyItem } from '@/components/core/property-item';
import { PropertyList } from '@/components/core/property-list';
import { toast } from '@/components/core/toaster';
import ErrorDisplay from '@/components/dashboard/error';
import { defaultLessonType, LessonTypeDefaultValue } from '@/components/dashboard/lesson_type/_constants';
// import { getLessonTypeById } from '@/components/dashboard/lesson_type/http-actions';
// import { LessonTypeDefaultValue, type LessonType } from '@/components/dashboard/lesson_type/ILessonType';
// import { defaultLessonType } from '@/components/dashboard/lesson_type/interfaces';
import { Notifications } from '@/components/dashboard/lesson_type/notifications';
import { Payments } from '@/components/dashboard/lesson_type/payments';
import type { Address } from '@/components/dashboard/lesson_type/shipping-address';
import { ShippingAddress } from '@/components/dashboard/lesson_type/shipping-address';
import { type LessonType } from '@/components/dashboard/lesson_type/types';
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 default function Page(): React.JSX.Element {
const { t } = useTranslation(['common', 'lesson_type']);
const { t } = useTranslation();
const router = useRouter();
//
const { type_id: typeId } = useParams<{ type_id: string }>();
const { cat_id: catId } = useParams<{ cat_id: string }>();
//
const [showLoading, setShowLoading] = React.useState<boolean>(true);
const [showError, setShowError] = React.useState<boolean>(false);
//
const [showLessonType, setShowLessonType] = React.useState<LessonType>(LessonTypeDefaultValue);
const [showLessonCategory, setShowLessonCategory] = React.useState<LessonCategory>(defaultLessonCategory);
function handleEditClick(): void {
router.push(paths.dashboard.lesson_types.edit(showLessonType.id));
function handleEditClick() {
router.push(paths.dashboard.lesson_categories.edit(showLessonCategory.id));
}
React.useEffect(() => {
if (typeId) {
pb.collection('LessonsTypes')
.getOne(typeId)
if (catId) {
pb.collection(COL_LESSON_CATEGORIES)
.getOne(catId)
.then((model: RecordModel) => {
setShowLessonType({ ...defaultLessonType, ...model });
setShowLessonCategory({ ...defaultLessonCategory, ...model });
})
.catch((err) => {
logger.error(err);
@@ -80,7 +83,7 @@ export default function Page(): React.JSX.Element {
setShowLoading(false);
});
}
}, [typeId]);
}, [catId]);
if (showLoading) return <FormLoading />;
@@ -108,31 +111,35 @@ export default function Page(): React.JSX.Element {
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.lesson_types.list}
href={paths.dashboard.lesson_categories.list}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
{t('list.title', { ns: 'lesson_type' })}
{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' }}>
<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">{showLessonType.name}</Typography>
<Typography variant="h4">{showLessonCategory.name}</Typography>
<Chip
icon={<CheckCircleIcon color="var(--mui-palette-success-main)" weight="fill" />}
label={showLessonType.visible}
label={showLessonCategory.visible}
size="small"
variant="outlined"
/>
</Stack>
<Typography color="text.secondary" variant="body1">
{showLessonType.id}
{showLessonCategory.id}
</Typography>
</div>
</Stack>
@@ -162,7 +169,7 @@ export default function Page(): React.JSX.Element {
<UserIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title={t('basic-details', { ns: 'lesson_type' })}
title={t('basic-details', { ns: 'lesson_category' })}
/>
<PropertyList
divider={<Divider />}
@@ -171,16 +178,15 @@ export default function Page(): React.JSX.Element {
>
{(
[
{ key: 'Customer ID', value: <Chip label={showLessonType.id} size="small" variant="soft" /> },
{ key: 'Name', value: showLessonType.name },
{ key: 'Type', value: showLessonType.type },
{ key: 'Pos', value: showLessonType.pos },
{ 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={showLessonType.visible}
label={showLessonCategory.visible}
size="small"
variant="soft"
/>
@@ -212,7 +218,7 @@ export default function Page(): React.JSX.Element {
<ShieldWarningIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title={t('security', { ns: 'lesson_type' })}
title={t('security', { ns: 'lesson_category' })}
/>
<CardContent>
<Stack spacing={1}>
@@ -222,7 +228,7 @@ export default function Page(): React.JSX.Element {
</Button>
</div>
<Typography color="text.secondary" variant="body2">
A deleted lesson type 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>
@@ -285,7 +291,7 @@ export default function Page(): React.JSX.Element {
<CreditCardIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title={t('billing-details', { ns: 'lesson_type' })}
title={t('billing-details', { ns: 'lesson_category' })}
/>
<CardContent>
<Card sx={{ borderRadius: 1 }} variant="outlined">
@@ -320,7 +326,7 @@ export default function Page(): React.JSX.Element {
<HouseIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title={t('shipping-addresses', { ns: 'lesson_type' })}
title={t('shipping-addresses', { ns: 'lesson_category' })}
/>
<CardContent>
<Grid container spacing={3}>

View File

@@ -1,5 +1,5 @@
import { dayjs } from '@/lib/dayjs';
import { LessonType } from '@/components/dashboard/lesson_type/types';
import { LessonType } from '@/components/dashboard/lesson_type/lesson-type';
// import type { LessonType } from '@/components/dashboard/lesson_type/ILessonType';

View File

@@ -1,5 +1,5 @@
import { dayjs } from '@/lib/dayjs';
import { LessonType } from '@/components/dashboard/lesson_type/types';
import { LessonType } from '@/components/dashboard/lesson_type/lesson-type';
// import type { LessonType } from '@/components/dashboard/lesson_type/ILessonType';

View File

@@ -1,5 +1,9 @@
'use client';
// RULES:
// contains list page for lesson_types (LessonTypes)
// contain definition to collection only
//
import * as React from 'react';
import { useRouter } from 'next/navigation';
import { COL_LESSON_TYPES } from '@/constants';
@@ -14,19 +18,18 @@ 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 { defaultLessonType } from '@/components/dashboard/lesson_type/_constants';
// import type { LessonType } from '@/components/dashboard/lesson_type/ILessonType';
// import { defaultLessonType, emptyLessonType, safeAssignment } from '@/components/dashboard/lesson_type/interfaces';
import type { LessonType } from '@/components/dashboard/lesson_type/lesson-type';
import { LessonTypesFilters } from '@/components/dashboard/lesson_type/lesson-types-filters';
import type { Filters } from '@/components/dashboard/lesson_type/lesson-types-filters';
import { LessonTypesPagination } from '@/components/dashboard/lesson_type/lesson-types-pagination';
import { LessonTypesSelectionProvider } from '@/components/dashboard/lesson_type/lesson-types-selection-context';
import { LessonTypesTable } from '@/components/dashboard/lesson_type/lesson-types-table';
import type { LessonType } from '@/components/dashboard/lesson_type/types';
import FormLoading from '@/components/loading';
export default function Page({ searchParams }: PageProps): React.JSX.Element {
@@ -38,14 +41,14 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
const [showLoading, setShowLoading] = React.useState<boolean>(true);
const [showError, setShowError] = React.useState<boolean>(false);
const [showError, setShowError] = React.useState({ show: false, detail: '' });
//
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
//
const [f, setF] = React.useState<LessonType[]>([]);
const [currentPage, setCurrentPage] = React.useState<number>(0);
//
const [recordCount, setRecordCount] = React.useState<number>(0);
const [listOption, setListOption] = React.useState({});
const [listSort, setListSort] = React.useState({});
@@ -63,15 +66,33 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
setLessonTypesData(tempLessonTypes);
setRecordCount(totalItems);
setF(tempLessonTypes);
// console.log({ currentPage, f });
} catch (error) {
//
logger.error(error);
setShowError({
//
show: true,
detail: JSON.stringify(error, null, 2),
});
} finally {
setShowLoading(false);
}
};
const [lastListOption, setLastListOption] = React.useState({});
const isFirstRun = React.useRef(false);
React.useEffect(() => {
void reloadRows();
if (!isFirstRun.current) {
isFirstRun.current = true;
} else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
// reset page number as tab changes
setLastListOption(listOption);
setCurrentPage(0);
void reloadRows();
} else {
void reloadRows();
}
}, [currentPage, rowsPerPage, listOption]);
React.useEffect(() => {
@@ -94,18 +115,32 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
tempFilter.push(`type ~ "%${type}%"`);
}
setListOption({
filter: tempFilter.join(' && '),
sort: tempSortDir,
//
});
let preFinalListOption = {};
if (tempFilter.length > 0) {
preFinalListOption = { filter: tempFilter.join(' && ') };
}
if (tempSortDir.length > 0) {
preFinalListOption = { ...preFinalListOption, sort: tempSortDir };
}
setListOption(preFinalListOption);
// setListOption({
// filter: tempFilter.join(' && '),
// sort: tempSortDir,
// //
// });
}, [visible, sortDir, name, type]);
if (f.length === 0 || showLoading) return <FormLoading />;
// return <>helloworld</>;
if (showError)
if (showLoading) return <FormLoading />;
if (showError.show)
return (
<ErrorDisplay message={t('unable-to-process-request')} code="500" details={t('detailed-error-information')} />
<ErrorDisplay
message={t('error.unable-to-process-request')}
code="500"
details={showError.detail}
/>
);
return (
@@ -118,7 +153,11 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
}}
>
<Stack spacing={4}>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={3}
sx={{ alignItems: 'flex-start' }}
>
<Box sx={{ flex: '1 1 auto' }}>
<Typography variant="h4">{t('list.title')}</Typography>
</Box>
@@ -133,6 +172,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
variant="contained"
>
{/* add new lesson type */}
{/* TODO: refactor translations.json */}
{t('dashboard.lessonTypes.add')}
</LoadingButton>
</Box>
@@ -146,7 +186,10 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
/>
<Divider />
<Box sx={{ overflowX: 'auto' }}>
<LessonTypesTable reloadRows={reloadRows} rows={f} />
<LessonTypesTable
reloadRows={reloadRows}
rows={f}
/>
</Box>
<Divider />
<LessonTypesPagination
@@ -159,6 +202,9 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
</Card>
</LessonTypesSelectionProvider>
</Stack>
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
<pre>{JSON.stringify(f, null, 2)}</pre>
</Box>
</Box>
);
}
@@ -220,6 +266,5 @@ interface PageProps {
name?: string;
visible?: string;
type?: string;
//
};
}

View File

@@ -0,0 +1,43 @@
'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(),
},
];

View File

@@ -0,0 +1,139 @@
'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 BasicDetailCard from '@/app/dashboard/Sample/BasicDetailCard';
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard';
import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard';
import SampleTitleCard from '@/app/dashboard/Sample/TitleCard';
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
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 { defaultLessonType, LessonTypeDefaultValue } from '@/components/dashboard/lesson_type/_constants';
import { type LessonType } from '@/components/dashboard/lesson_type/lesson-type';
import { Notifications } from '@/components/dashboard/lesson_type/notifications';
import FormLoading from '@/components/loading';
export default function Page(): React.JSX.Element {
const { t } = useTranslation(['lesson_type']);
const router = useRouter();
//
const { type_id: typeId } = useParams<{ type_id: string }>();
//
const [showLoading, setShowLoading] = React.useState<boolean>(true);
const [showError, setShowError] = React.useState<boolean>(false);
const [errorDetails, setErrorDetails] = React.useState('');
//
const [showLessonType, setShowLessonType] = React.useState<LessonType>(LessonTypeDefaultValue);
function handleEditClick(): void {
router.push(paths.dashboard.lesson_types.edit(showLessonType.id));
}
React.useEffect(() => {
if (typeId) {
pb.collection('LessonsTypes')
.getOne(typeId)
.then((model: RecordModel) => {
setShowLessonType({ ...defaultLessonType, ...model });
})
.catch((err) => {
logger.error(err);
toast(t('dashboard.lessonTypes.list.error'));
setErrorDetails(err);
setShowError(true);
})
.finally(() => {
setShowLoading(false);
});
}
}, [typeId]);
if (showLoading) return <FormLoading />;
if (showError)
return (
<ErrorDisplay
message={t('error.unable-to-process-request', { ns: 'common' })}
code="500"
details={JSON.stringify(errorDetails, null, 2)}
/>
);
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_types.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' }}
>
<SampleTitleCard />
</Stack>
</Stack>
<Grid
container
spacing={4}
>
<Grid
lg={4}
xs={12}
>
<Stack spacing={4}>
<BasicDetailCard
lpCatId={showLessonType.id}
handleEditClick={handleEditClick}
/>
<SampleSecurityCard />
</Stack>
</Grid>
<Grid
lg={8}
xs={12}
>
<Stack spacing={4}>
<SamplePaymentCard />
<SampleAddressCard />
<Notifications notifications={SampleNotifications} />
</Stack>
</Grid>
</Grid>
</Stack>
</Box>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -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 { LpCategoryCreateForm } from '@/components/dashboard/lp/categories/lp-category-create-form';
export default function Page(): React.JSX.Element {
// RULES: follow the name of page directory
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.lp_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>
<LpCategoryCreateForm />
</Stack>
</Box>
);
}

View File

@@ -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 { LpCategoryEditForm } from '@/components/dashboard/lp/categories/lp-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.lp_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>
<LpCategoryEditForm />
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,274 @@
'use client';
// RULES:
// contains list page for lp_categories (QuizLPCategories)
// contain definition to collection only
//
import * as React from 'react';
import { useRouter } from 'next/navigation';
import { COL_QUIZ_LP_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 { defaultLpCategory } from '@/components/dashboard/lp/categories/_constants';
import { LpCategoriesFilters } from '@/components/dashboard/lp/categories/lp-categories-filters';
import type { Filters } from '@/components/dashboard/lp/categories/lp-categories-filters';
import { LpCategoriesPagination } from '@/components/dashboard/lp/categories/lp-categories-pagination';
import { LpCategoriesSelectionProvider } from '@/components/dashboard/lp/categories/lp-categories-selection-context';
import { LpCategoriesTable } from '@/components/dashboard/lp/categories/lp-categories-table';
import type { LpCategory } from '@/components/dashboard/lp/categories/type';
import FormLoading from '@/components/loading';
export default function Page({ searchParams }: PageProps): React.JSX.Element {
const { t } = useTranslation(['lp_categories']);
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>(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_LP_CATEGORIES)
.getList(currentPage + 1, rowsPerPage, listOption);
const { items, totalItems } = models;
const tempLessonTypes: LpCategory[] = items.map((lt) => {
return { ...defaultLpCategory, ...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.lp_categories.create);
}}
startIcon={<PlusIcon />}
variant="contained"
>
{t('list.add')}
</LoadingButton>
</Box>
</Stack>
<LpCategoriesSelectionProvider lessonCategories={f}>
<Card>
<LpCategoriesFilters
filters={{ email, phone, status, name, visible, type }}
fullData={lessonCategoriesData}
sortDir={sortDir}
/>
<Divider />
<Box sx={{ overflowX: 'auto' }}>
<LpCategoriesTable
reloadRows={reloadRows}
rows={f}
/>
</Box>
<Divider />
<LpCategoriesPagination
count={recordCount}
page={currentPage}
rowsPerPage={rowsPerPage}
setPage={setCurrentPage}
setRowsPerPage={setRowsPerPage}
/>
</Card>
</LpCategoriesSelectionProvider>
</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: LpCategory[], sortDir: 'asc' | 'desc' | undefined): LpCategory[] {
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: LpCategory[], { email, phone, status, name, visible }: Filters): LpCategory[] {
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;
//
};
}

View File

@@ -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[];

View File

@@ -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>
);
}

View File

@@ -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>
</>
);
}

View 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_LP_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 { defaultLpCategory } from '@/components/dashboard/lp/categories/_constants.ts';
import { Notifications } from '@/components/dashboard/lp/categories/notifications';
import type { LpCategory } from '@/components/dashboard/lp/categories/type';
import FormLoading from '@/components/loading';
import BasicDetailCard from './BasicDetailCard';
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<LpCategory>(defaultLpCategory);
function handleEditClick() {
router.push(paths.dashboard.lp_categories.edit(showLessonCategory.id));
}
React.useEffect(() => {
if (catId) {
pb.collection(COL_QUIZ_LP_CATEGORIES)
.getOne(catId)
.then((model: RecordModel) => {
setShowLessonCategory({ ...defaultLpCategory, ...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.lp_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>
);
}

View File

@@ -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 { LpQuestionCreateForm } from '@/components/dashboard/lp/questions/lp-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>
<LpQuestionCreateForm />
</Stack>
</Box>
);
}

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