This commit is contained in:
louiscklaw
2025-04-26 06:15:18 +08:00
parent df87cfb037
commit 6e8fea3bdd
57 changed files with 1392 additions and 20183 deletions

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,8 @@ they will:
## project background and initial setup ## project background and initial setup
- No need to reply me what you are going on and your digest in this phase. - **IMPORTANT**: No need to reply me what you are going on and your digest in this phase.
No need to show me your code plan
Just reply me "OK" when done Just reply me "OK" when done
- base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project` - base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project`
@@ -36,9 +37,13 @@ they will:
- look into the md files in folder `<base_dir>/002_source/cms/_AI_WORKSPACE/001_guideline` - look into the md files in folder `<base_dir>/002_source/cms/_AI_WORKSPACE/001_guideline`
- directory may contain `repomix-output.xml` file, that is a simple summary of all files inside the directory
- if the directory user provided contins `_GUIDELINES.md`, please read the file - if the directory user provided contins `_GUIDELINES.md`, please read the file
- read the files, remember and link up the ideas in file stated above, - read the files, remember and link up the ideas in file stated above, i will tell them the task afterwards
i will tell them the task afterwards
- please review at least 3 times after you modified the code
## frameworks documentation and samples
- `MUI`
- `<base_dir>/002_source/cms/src/components/widgets/forms` contains sample forms,

View File

@@ -5,16 +5,15 @@ tags: db, driver
# clone db driver # clone db driver
please understand the tsx files in folder please understand the tsx files in folder
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Notifications` `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/vocabularies.draft`
change all occurrence of `customer` to `notification` thanks. change all occurrence of `lessonCategories` to `vocabularies` thanks.
## FAQ ## FAQ
1. 在constants.ts中是否已定义COL_NOTIFICATIONS常量?没有,需要先定义它。 1. `constants.ts` 中是否已定义 `COL_VOCABULARIES` 常量?没有,需要先定义它。
2. Notification相关的类型定义在哪里?是在现有文件中还是需要新建? 需要新建 2. Vocabulary 相关的类型定义在哪里?是在现有文件中还是需要新建? 需要新建
3. 是否需要保留原始Customer驱动文件还是完全替换为Notification驱动 3. 是否需要保留原始 `lessonCategories` 驱动文件? 不需要
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Notifications` 中的 code
请确认这些信息,以便我制定完整的修改方案。 请确认这些信息,以便我制定完整的修改方案。

View File

View File

@@ -1,39 +0,0 @@
# PROMPT
please update `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/public/locales/dev/translation.json`
to add below translations, thanks.
```
dashboard.lessonCategories.edit.title
dashboard.lessonCategories.edit.avatar
dashboard.lessonCategories.edit.avatarRequirements
dashboard.lessonCategories.edit.name
dashboard.lessonCategories.edit.type
```
---
please read `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lesson_category/lesson-category-edit-form.tsx`
and update `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/public/locales/dev/translation.json` the missing translations, thanks.
---
please refactor `common.json` to smaller translation files, thanks
e.g. `lessonTypes` -> `lesson_type`
---
Hi, i want you to help merge two translation files.
base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/public/locales/dev`
I want you to merge the content
from `<base_dir>/lesson_type.json` (source file)
to `<base_dir>/listening_practice.json` (dest file)
please extract , link up and remember the document properties
(e.g. types, functions, variables, constants, etc)
update the variables and properties of dest file to reflect `listening practice categories`/`lp_categories`

View File

@@ -1,135 +1 @@
{ {}
"languageChanged": "語言已更改",
"type": "類型",
"helloworld": "你好,世界",
"Add": "新增項目",
"visible": "可視",
"hidden": "屏閉",
"Hidden": "屏閉",
"Newest": "最新",
"Oldest": "最舊",
"Lesson position": "位置",
"Lesson type": "種類",
"Type": "種類",
"Name": "名稱",
"Filter by name": "依名稱過濾",
"Filter by type": "依類型過濾",
"Actions": "動作",
"Created at": "建立時間",
"Updated at": "更新時間",
"Position": "位置",
"Visible": "顯示",
"Apply": "套用",
"Overview": "概觀",
"Dashboard": "儀表板",
"Tickets": "票券",
"Sign ups": "註冊",
"Open issues": "開啟問題",
"Closed issues": "關閉問題",
"increase": "增加",
"decrease": "減少",
"common.loading": "載入中",
"vs last month": "與上個月相比",
"Dashboards": "儀表板",
"App usage": "應用程式使用情況",
"Find your dream job": "找到你的夢想工作",
"Need help figuring things out?": "需要幫助嗎?",
"Search for jobs that match your skills and apply to them directly": "搜索符合你技能的工作,直接申請",
"Find answers to your questions and get in touch with our team.": "找到你的問題並與我們的團隊聯繫",
"Explore documentation": "探索文件",
"Learn how to get started with our product and make the most of it.": "學習如何開始使用我們的產品並充分利用它",
"Search Jobs": "搜索工作",
"Help Center": "幫助中心",
"Documentation": "文件",
"increase_in_app_usage_with": "使用者量增加",
"new_products_purchased": "新產品購買",
"Our subscriptions": "我們的訂閱",
"this_year": "今年",
"is_forecasted_to_increase_in_your_traffic_by_the_end_of_the_current_month": "今年預計增加你的流量",
"Jan": "一月",
"Feb": "二月",
"Mar": "三月",
"Apr": "四月",
"May": "五月",
"Jun": "六月",
"Jul": "七月",
"Aug": "八月",
"Sep": "九月",
"Oct": "十月",
"Nov": "十一月",
"Dec": "十二月",
"See all subscriptions": "查看所有訂閱",
"app_chat": "應用程式聊天",
"Upcoming events": "即將發生的事件",
"Based on the linked bank accounts": "基於連結的銀行帳戶",
"App limits": "應用程式限制",
"You have used {percentage} of your available spots.": "您已使用 {percentage} 的可用座位。升級計劃以建立更多專案",
"userMessagesUnread": "hello userMessagesUnread",
"Upgrade plan to create more projects": "升級計劃以建立更多專案",
"You have almost reached your limit": "您已接近您的限制",
"Upgrade plan": "升級計劃",
"Help center": "幫助中心",
"Jobs": "工作",
"go_to_chat": "前往聊天",
"See all events": "查看所有事件",
"You have used": "您已使用",
"of your available spots": "的可用座位",
"Paid": "已付款",
"Canceled": "已取消",
"Pending": "待付款",
"Expiring": "已過期",
"Refunded": "已退款",
"Total": "總計",
"Total paid": "已付款",
"Total cancelled": "已取消",
"Total pending": "待付款",
"year": "年",
"month": "月",
"week": "週",
"day": "日",
"hour": "小時",
"minute": "分鐘",
"second": "秒",
"ago": "前",
"remaining": "剩下",
"days": "天",
"hours": "小時",
"minutes": "分鐘",
"seconds": "秒",
"Clear filters": "清除篩選",
"Type information": "輸入資訊",
"Cancel": "取消",
"Delete": "刪除",
"All": "全部",
"categories": "課程分類",
"loading": "載入中",
"Notifications": "通知",
"Mark all as read": "標記所有為已讀",
"There are no notifications": "沒有通知",
"listenings": "聽講 (Listenings)",
"question_category": "問題分類",
"question_list": "問題",
"matching_frenzy": "配對 (Matching Frenzy)",
"connective_revision": "連接詞 (Connective Revision)",
"settings": "設定",
"students": "學生",
"dashboard.lessonCategories.title": "課程分類",
"dashboard.lessonCategorys.edit.name": "課程分類名稱",
"dashboard.lessonCategorys.edit.position": "課程分類順序",
"dashboard.lessonCategorys.edit.title": "編輯課程分類",
"dashboard.lessonCategorys.edit.visibleToUser": "顯示給使用者",
"dashboard.lessonCategorys.edit.visible": "顯示",
"dashboard.lessonCategorys.edit.hidden": "隱藏",
"dashboard.lessonCategorys.edit.cancelButton": "取消",
"dashboard.lessonCategorys.edit.updateButton": "更新",
"dashboard.lessonCategorys.list.title": "課堂種類",
"word-count": "字數",
"unable-to-process-request": "無法處理請求",
"detailed-error-information": "詳細錯誤資訊",
"error": {
"unable-to-process-request": "無法處理您的請求",
"detailed-error-information": "詳細錯誤資訊"
},
"name-is-required": "名稱為必填",
"listening-practice": "聽講練習"
}

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": "課程類型列表1",
"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

@@ -1,36 +0,0 @@
{
"add": "新增",
"listening-practice": "聽力練習",
"list": {
"title": "聽力練習分類列表",
"empty": "沒有找到聽力練習分類",
"action": "動作?",
"basic-details": "基本資料"
},
"helloworld": "listening_practice",
"title": "聽力練習分類",
"type": "聽力練習分類",
"error": {
"invalid": "無效的聽力練習分類",
"not_found": "未找到聽力練習分類"
},
"create": {
"title": "創建聽力練習分類",
"success": "聽力練習分類創建成功",
"error": "創建聽力練習分類時出錯"
},
"edit": {
"title": "編輯聽力練習分類",
"success": "聽力練習分類更新成功",
"error": "更新聽力練習分類時出錯"
},
"delete": {
"title": "刪除聽力練習分類",
"confirm": "確定要刪除聽力練習分類嗎?",
"success": "聽力練習分類刪除成功",
"error": "刪除聽力練習分類時出錯"
},
"view": {
"title": "查看聽力練習分類詳情"
}
}

View File

@@ -1,203 +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.lessonCategories.list": "詞語種類",
"dashboard.vocabulary.list": "詞語種類",
"dashboard.connective.list": "詞語種類",
"dashboard.lessonTypes.list.title": "課程類型列表3",
"dashboard.lessonTypes.list.message": "請選擇課程類型",
"dashboard.lessonTypes.list.empty": "目前沒有課程類型",
"dashboard.lessonTypes.list.empty.title": "目前沒有課程類型",
"dashboard.lessonTypes.list.empty.message": "請建立課程類型",
"dashboard.lessonTypes.list.empty.create": "建立課程類型",
"Lesson Name": "課程名稱",
"dashboard.lessonTypes.list.empty.create.title": "建立課程類型",
"dashboard.lessonTypes.list.empty.create.message": "請輸入課程名稱",
"dashboard.lessonTypes.list.empty.create.success": "課程建立成功",
"dashboard.lessonTypes.list.empty.create.error": "課程建立失敗",
"dashboard.lessonTypes.list.empty.create.name": "課程名稱",
"dashboard.lessonTypes.list.empty.create.type": "課程類型",
"dashboard.lessonTypes.list.empty.create.type.placeholder": "請選擇課程類型",
"dashboard.lessonTypes.list.empty.create.type.title": "課程類型",
"dashboard.lessonTypes.list.empty.create.type.message": "請選擇課程類型",
"dashboard.lessonTypes.list.empty.create.type.empty": "目前沒有課程類型",
"dashboard.lessonTypes.list.empty.create.type.empty.title": "目前沒有課程類型",
"dashboard.lessonTypes.list.empty.create.type.empty.message": "請建立課程類型",
"dashboard.lessonTypes.list.empty.create.type.empty.create": "建立課程類型",
"Position": "位置",
"Visible": "顯示",
"Lesson Types": "課程類型",
"Apply": "套用",
"Overview": "概觀",
"Dashboard": "儀表板",
"Tickets": "票券",
"Sign ups": "註冊",
"Open issues": "開啟問題",
"Closed issues": "關閉問題",
"increase": "增加",
"decrease": "減少",
"common.loading": "載入中",
"vs last month": "與上個月相比",
"Dashboards": "儀表板",
"App usage": "應用程式使用情況",
"Find your dream job": "找到你的夢想工作",
"Need help figuring things out?": "需要幫助嗎?",
"Search for jobs that match your skills and apply to them directly": "搜索符合你技能的工作,直接申請",
"Find answers to your questions and get in touch with our team.": "找到你的問題並與我們的團隊聯繫",
"Explore documentation": "探索文件",
"Learn how to get started with our product and make the most of it.": "學習如何開始使用我們的產品並充分利用它",
"Search Jobs": "搜索工作",
"Help Center": "幫助中心",
"Documentation": "文件",
"increase_in_app_usage_with": "使用者量增加",
"new_products_purchased": "新產品購買",
"Our subscriptions": "我們的訂閱",
"this_year": "今年",
"is_forecasted_to_increase_in_your_traffic_by_the_end_of_the_current_month": "今年預計增加你的流量",
"Jan": "一月",
"Feb": "二月",
"Mar": "三月",
"Apr": "四月",
"May": "五月",
"Jun": "六月",
"Jul": "七月",
"Aug": "八月",
"Sep": "九月",
"Oct": "十月",
"Nov": "十一月",
"Dec": "十二月",
"See all subscriptions": "查看所有訂閱",
"app_chat": "應用程式聊天",
"Upcoming events": "即將發生的事件",
"Based on the linked bank accounts": "基於連結的銀行帳戶",
"App limits": "應用程式限制",
"You have used {percentage} of your available spots.": "您已使用 {percentage} 的可用座位。升級計劃以建立更多專案",
"userMessagesUnread": "hello userMessagesUnread",
"Upgrade plan to create more projects": "升級計劃以建立更多專案",
"You have almost reached your limit": "您已接近您的限制",
"Upgrade plan": "升級計劃",
"Help center": "幫助中心",
"Jobs": "工作",
"go_to_chat": "前往聊天",
"See all events": "查看所有事件",
"You have used": "您已使用",
"of your available spots": "的可用座位",
"Paid": "已付款",
"Canceled": "已取消",
"Pending": "待付款",
"Expiring": "已過期",
"Refunded": "已退款",
"Total": "總計",
"Total paid": "已付款",
"Total cancelled": "已取消",
"Total pending": "待付款",
"year": "年",
"month": "月",
"week": "週",
"day": "日",
"hour": "小時",
"minute": "分鐘",
"second": "秒",
"ago": "前",
"remaining": "剩下",
"days": "天",
"hours": "小時",
"minutes": "分鐘",
"seconds": "秒",
"Clear filters": "清除篩選",
"Type information": "輸入資訊",
"dashboard.lessonTypes.create.typeInformation": "輸入課程類型資訊",
"dashboard.lessonTypes.create.avatar": "上傳課程類型圖片",
"dashboard.lessonTypes.create.avatarRequirements": "上傳課程類型圖片",
"dashboard.lessonTypes.create.select": "請選擇",
"dashboard.lessonTypes.create.name": "課程類型名稱",
"dashboard.lessonTypes.create.type": "課程類型",
"dashboard.lessonTypes.create.position": "課程類型順序",
"dashboard.lessonTypes.create.visibleToUser": "顯示給使用者",
"dashboard.lessonTypes.edit.typeInformation": "輸入課程類型資訊",
"dashboard.lessonTypes.edit.avatar": "上傳課程類型圖片",
"dashboard.lessonTypes.edit.avatarRequirements": "上傳課程類型圖片",
"dashboard.lessonTypes.edit.select": "請選擇",
"dashboard.lessonTypes.edit.name": "課程類型名稱",
"dashboard.lessonTypes.edit.type": "課程類型",
"dashboard.lessonTypes.edit.position": "課程類型順序",
"dashboard.lessonTypes.edit.visibleToUser": "顯示給使用者",
"dashboard.lessonTypes.edit.visible": "顯示",
"dashboard.lessonTypes.edit.hidden": "隱藏",
"dashboard.lessonTypes.edit.cancelButton": "取消",
"dashboard.lessonTypes.edit.updateButton": "更新",
"Delete Lesson Type ?": "刪除課程類型?",
"Are you sure you want to delete lesson type ?": "確定要刪除課程類型嗎?",
"Cancel": "取消",
"Delete": "刪除",
"All": "全部",
"categories": "問題分類",
"listening-practice": "聽講練習 (LP)",
"matching-frenzy": "配對練習 (MF)",
"connective-revision": "連接詞練習 (CR)",
"teachers": "導師",
"questions": "題目",
"loading": "載入中",
"Notifications": "通知",
"Mark all as read": "標記所有為已讀",
"There are no notifications": "沒有通知",
"listenings": "聽講 (Listenings)",
"question_category": "問題分類",
"question_list": "問題",
"matching_frenzy": "配對 (Matching Frenzy)",
"connective_revision": "連接詞 (Connective Revision)",
"settings": "設定",
"students": "學生",
"dashboard.lessonCategories.add": "新增課程分類",
"dashboard.lessonCategories.title": "課程分類",
"dashboard.lessonCategorys.edit.name": "課程分類名稱",
"dashboard.lessonCategorys.edit.position": "課程分類順序",
"dashboard.lessonCategorys.edit.title": "編輯課程分類",
"dashboard.lessonCategorys.edit.visibleToUser": "顯示給使用者",
"dashboard.lessonCategorys.edit.visible": "顯示",
"dashboard.lessonCategorys.edit.hidden": "隱藏",
"dashboard.lessonCategorys.edit.cancelButton": "取消",
"dashboard.lessonCategorys.edit.updateButton": "更新",
"dashboard.lessonCategorys.list.title": "課堂種類",
"word-count": "字數",
"dashboard.lessonTypes.list.error": "課程類型載入失敗",
"unable-to-process-request": "無法處理請求",
"detailed-error-information": "詳細錯誤資訊"
}

View File

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

View File

@@ -44,7 +44,6 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
const { email, phone, sortDir, status } = searchParams; const { email, phone, sortDir, status } = searchParams;
const [lessonCategoriesData, setLessonCategoriesData] = React.useState<Teacher[]>([]); const [lessonCategoriesData, setLessonCategoriesData] = React.useState<Teacher[]>([]);
//
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false); const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
const [showLoading, setShowLoading] = React.useState<boolean>(true); const [showLoading, setShowLoading] = React.useState<boolean>(true);
@@ -73,12 +72,9 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
setLessonCategoriesData(tempLessonTypes); setLessonCategoriesData(tempLessonTypes);
setRecordCount(totalItems); setRecordCount(totalItems);
setF(tempLessonTypes); setF(tempLessonTypes);
// console.log({ currentPage, f });
} catch (error) { } catch (error) {
//
logger.error(error); logger.error(error);
setShowError({ setShowError({
//
show: true, show: true,
detail: JSON.stringify(error, null, 2), detail: JSON.stringify(error, null, 2),
}); });
@@ -129,11 +125,6 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
preFinalListOption = { ...preFinalListOption, sort: tempSortDir }; preFinalListOption = { ...preFinalListOption, sort: tempSortDir };
} }
setListOption(preFinalListOption); setListOption(preFinalListOption);
// setListOption({
// filter: tempFilter.join(' && '),
// sort: tempSortDir,
// //
// });
}, [sortDir, email, phone, status]); }, [sortDir, email, phone, status]);
if (showLoading) return <FormLoading />; if (showLoading) return <FormLoading />;
@@ -211,42 +202,6 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
); );
} }
// Sorting and filtering has to be done on the server.
function applySort(row: Teacher[], sortDir: 'asc' | 'desc' | undefined): Teacher[] {
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: Teacher[], { email, phone, status }: Filters): Teacher[] {
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 { interface PageProps {
searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string }; searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string };
} }

View File

@@ -10,7 +10,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { paths } from '@/paths'; import { paths } from '@/paths';
import { LessonCategoryCreateForm } from '@/components/dashboard/lesson_category/lesson-category-create-form'; import { VocabularyCreateForm } from '@/components/dashboard/vocabulary/vocabulary-create-form';
export default function Page(): React.JSX.Element { export default function Page(): React.JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -29,19 +29,19 @@ export default function Page(): React.JSX.Element {
<Link <Link
color="text.primary" color="text.primary"
component={RouterLink} component={RouterLink}
href={paths.dashboard.lesson_categories.list} href={paths.dashboard.vocabularies.list}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }} sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2" variant="subtitle2"
> >
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" /> <ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
{t('title', { ns: 'lesson_category' })} {t('title',)}
</Link> </Link>
</div> </div>
<div> <div>
<Typography variant="h4">{t('create.title', { ns: 'lesson_category' })}</Typography> <Typography variant="h4">{t('create.title', )}</Typography>
</div> </div>
</Stack> </Stack>
<LessonCategoryCreateForm /> <VocabularyCreateForm />
</Stack> </Stack>
</Box> </Box>
); );

View File

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

View File

@@ -10,10 +10,10 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { paths } from '@/paths'; import { paths } from '@/paths';
import { LessonCategoryEditForm } from '@/components/dashboard/lesson_category/lesson-category-edit-form'; import { VocabularyEditForm } from '@/components/dashboard/vocabulary/vocabulary-edit-form';
export default function Page(): React.JSX.Element { export default function Page(): React.JSX.Element {
const { t } = useTranslation(['lesson_category']); const { t } = useTranslation();
return ( return (
<Box <Box
@@ -30,19 +30,19 @@ export default function Page(): React.JSX.Element {
<Link <Link
color="text.primary" color="text.primary"
component={RouterLink} component={RouterLink}
href={paths.dashboard.lesson_categories.list} href={paths.dashboard.vocabularies.list}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }} sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2" variant="subtitle2"
> >
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" /> <ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
{t('edit.title', { ns: 'lesson_category' })} {t('edit.title')}
</Link> </Link>
</div> </div>
<div> <div>
<Typography variant="h4">{t('edit.title', { ns: 'lesson_category' })}</Typography> <Typography variant="h4">{t('edit.title')}</Typography>
</div> </div>
</Stack> </Stack>
<LessonCategoryEditForm /> <VocabularyEditForm />
</Stack> </Stack>
</Box> </Box>
); );

View File

@@ -1,12 +1,11 @@
'use client'; 'use client';
// RULES: // RULES:
// contains list page for lesson_categories (LessonCategories) // contains list page for vocabularies (Vocabularies)
// contain definition to collection only
// //
import * as React from 'react'; import * as React from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { COL_LESSON_CATEGORIES } from '@/constants'; import { COL_VOCABULARIES } from '@/constants';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Card from '@mui/material/Card'; import Card from '@mui/material/Card';
@@ -21,25 +20,25 @@ import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development'; import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger'; import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb'; import { pb } from '@/lib/pb';
import { toast } from '@/components/core/toaster'; // import { toast } from '@/components/core/toaster';
import ErrorDisplay from '@/components/dashboard/error'; import ErrorDisplay from '@/components/dashboard/error';
import { defaultLessonCategory } from '@/components/dashboard/lesson_category/_constants'; import { defaultVocabulary } from '@/components/dashboard/vocabulary/_constants';
import { LessonCategoriesFilters } from '@/components/dashboard/lesson_category/lesson-categories-filters'; import { VocabulariesFilters } from '@/components/dashboard/vocabulary/vocabularies-filters';
import type { Filters } from '@/components/dashboard/lesson_category/lesson-categories-filters'; import type { Filters } from '@/components/dashboard/vocabulary/vocabularies-filters';
import { LessonCategoriesPagination } from '@/components/dashboard/lesson_category/lesson-categories-pagination'; import { VocabulariesPagination } from '@/components/dashboard/vocabulary/vocabularies-pagination';
import { LessonCategoriesSelectionProvider } from '@/components/dashboard/lesson_category/lesson-categories-selection-context'; import { VocabulariesSelectionProvider } from '@/components/dashboard/vocabulary/vocabularies-selection-context';
import { LessonCategoriesTable } from '@/components/dashboard/lesson_category/lesson-categories-table'; import { VocabulariesTable } from '@/components/dashboard/vocabulary/vocabularies-table';
import type { LessonCategory } from '@/components/dashboard/lesson_category/type'; import type { Vocabulary } from '@/components/dashboard/vocabulary/type';
// import type { LessonCategory } from '@/components/dashboard/lp_categories/type'; // import type { Vocabulary } from '@/components/dashboard/lp_categories/type';
import FormLoading from '@/components/loading'; import FormLoading from '@/components/loading';
// import { lessonCategoriesSampleData } from './lesson-categories-sample-data'; // import { lessonCategoriesSampleData } from './lesson-categories-sample-data';
export default function Page({ searchParams }: PageProps): React.JSX.Element { export default function Page({ searchParams }: PageProps): React.JSX.Element {
const { t } = useTranslation(['lesson_category']); const { t } = useTranslation();
const { email, phone, sortDir, status, name, visible, type } = searchParams; const { email, phone, sortDir, status, name, visible, type } = searchParams;
const router = useRouter(); const router = useRouter();
const [lessonCategoriesData, setLessonCategoriesData] = React.useState<LessonCategory[]>([]); const [lessonCategoriesData, setLessonCategoriesData] = React.useState<Vocabulary[]>([]);
// //
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false); const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
@@ -48,69 +47,39 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
// //
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5); const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
// //
const [f, setF] = React.useState<LessonCategory[]>([]); const [f, setF] = React.useState<Vocabulary[]>([]);
const [currentPage, setCurrentPage] = React.useState<number>(1); const [currentPage, setCurrentPage] = React.useState<number>(1);
// //
const [recordCount, setRecordCount] = React.useState<number>(0); const [recordCount, setRecordCount] = React.useState<number>(0);
const [listOption, setListOption] = React.useState({}); const [listOption, setListOption] = React.useState({});
const [listSort, setListSort] = React.useState({}); const [listSort, setListSort] = React.useState({});
//
const sortedLessonCategories = applySort(lessonCategoriesData, sortDir);
const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status });
// //
const reloadRows = async (): Promise<void> => { const reloadRows = async (): Promise<void> => {
try { try {
const models: ListResult<RecordModel> = await pb const models: ListResult<RecordModel> = await pb.collection(COL_VOCABULARIES).getList(1, 5, {
.collection(COL_LESSON_CATEGORIES) expand: 'cat_id',
.getList(currentPage + 1, rowsPerPage, listOption);
const { items, totalItems } = models;
const tempLessonTypes: LessonCategory[] = items.map((lt) => {
return { ...defaultLessonCategory, ...lt };
}); });
setLessonCategoriesData(tempLessonTypes); const { items, totalItems } = models;
const tempVocabularies: Vocabulary[] = items.map((v) => {
return { ...defaultVocabulary, ...v };
});
setLessonCategoriesData(tempVocabularies);
setRecordCount(totalItems); setRecordCount(totalItems);
setF(tempLessonTypes); setF(tempVocabularies);
// console.log({ currentPage, f }); // console.log({ currentPage, f });
} catch (error) { } catch (error) {
//
logger.error(error); logger.error(error);
setShowError({ setShowError({
//
show: true, show: true,
detail: JSON.stringify(error, null, 2), detail: JSON.stringify(error, null, 2),
}); });
} finally { } finally {
setShowLoading(false); 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 [lastListOption, setLastListOption] = React.useState({});
@@ -156,11 +125,6 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
preFinalListOption = { ...preFinalListOption, sort: tempSortDir }; preFinalListOption = { ...preFinalListOption, sort: tempSortDir };
} }
setListOption(preFinalListOption); setListOption(preFinalListOption);
// setListOption({
// filter: tempFilter.join(' && '),
// sort: tempSortDir,
// //
// });
}, [visible, sortDir, name, type]); }, [visible, sortDir, name, type]);
// return <>helloworld</>; // return <>helloworld</>;
@@ -199,7 +163,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
loading={isLoadingAddPage} loading={isLoadingAddPage}
onClick={(): void => { onClick={(): void => {
setIsLoadingAddPage(true); setIsLoadingAddPage(true);
router.push(paths.dashboard.lesson_categories.create); router.push(paths.dashboard.vocabularies.create);
}} }}
startIcon={<PlusIcon />} startIcon={<PlusIcon />}
variant="contained" variant="contained"
@@ -209,22 +173,22 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
</LoadingButton> </LoadingButton>
</Box> </Box>
</Stack> </Stack>
<LessonCategoriesSelectionProvider lessonCategories={f}> <VocabulariesSelectionProvider lessonCategories={f}>
<Card> <Card>
<LessonCategoriesFilters <VocabulariesFilters
filters={{ email, phone, status, name, visible, type }} filters={{ email, phone, status, name, visible, type }}
fullData={lessonCategoriesData} fullData={lessonCategoriesData}
sortDir={sortDir} sortDir={sortDir}
/> />
<Divider /> <Divider />
<Box sx={{ overflowX: 'auto' }}> <Box sx={{ overflowX: 'auto' }}>
<LessonCategoriesTable <VocabulariesTable
reloadRows={reloadRows} reloadRows={reloadRows}
rows={f} rows={f}
/> />
</Box> </Box>
<Divider /> <Divider />
<LessonCategoriesPagination <VocabulariesPagination
count={recordCount} count={recordCount}
page={currentPage} page={currentPage}
rowsPerPage={rowsPerPage} rowsPerPage={rowsPerPage}
@@ -232,7 +196,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
setRowsPerPage={setRowsPerPage} setRowsPerPage={setRowsPerPage}
/> />
</Card> </Card>
</LessonCategoriesSelectionProvider> </VocabulariesSelectionProvider>
</Stack> </Stack>
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}> <Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
<pre>{JSON.stringify(f, null, 2)}</pre> <pre>{JSON.stringify(f, null, 2)}</pre>
@@ -243,51 +207,51 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
// Sorting and filtering has to be done on the server. // Sorting and filtering has to be done on the server.
function applySort(row: LessonCategory[], sortDir: 'asc' | 'desc' | undefined): LessonCategory[] { // function applySort(row: Vocabulary[], sortDir: 'asc' | 'desc' | undefined): Vocabulary[] {
return row.sort((a, b) => { // return row.sort((a, b) => {
if (sortDir === 'asc') { // if (sortDir === 'asc') {
return a.createdAt.getTime() - b.createdAt.getTime(); // return a.createdAt.getTime() - b.createdAt.getTime();
} // }
return b.createdAt.getTime() - a.createdAt.getTime(); // return b.createdAt.getTime() - a.createdAt.getTime();
}); // });
} // }
function applyFilters(row: LessonCategory[], { email, phone, status, name, visible }: Filters): LessonCategory[] { // function applyFilters(row: Vocabulary[], { email, phone, status, name, visible }: Filters): Vocabulary[] {
return row.filter((item) => { // return row.filter((item) => {
if (email) { // if (email) {
if (!item.email?.toLowerCase().includes(email.toLowerCase())) { // if (!item.email?.toLowerCase().includes(email.toLowerCase())) {
return false; // return false;
} // }
} // }
if (phone) { // if (phone) {
if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) { // if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) {
return false; // return false;
} // }
} // }
if (status) { // if (status) {
if (item.status !== status) { // if (item.status !== status) {
return false; // return false;
} // }
} // }
if (name) { // if (name) {
if (!item.name?.toLowerCase().includes(name.toLowerCase())) { // if (!item.name?.toLowerCase().includes(name.toLowerCase())) {
return false; // return false;
} // }
} // }
if (visible) { // if (visible) {
if (!item.visible?.toLowerCase().includes(visible.toLowerCase())) { // if (!item.visible?.toLowerCase().includes(visible.toLowerCase())) {
return false; // return false;
} // }
} // }
return true; // return true;
}); // });
} // }
interface PageProps { interface PageProps {
searchParams: { searchParams: {

View File

@@ -4,7 +4,7 @@ import * as React from 'react';
// import type { Metadata } from 'next'; // import type { Metadata } from 'next';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { COL_LESSON_CATEGORIES } from '@/constants'; import { COL_VOCABULARIES } from '@/constants';
import Avatar from '@mui/material/Avatar'; import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
@@ -28,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 { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning'; import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning';
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
import { RecordModel } from 'pocketbase'; import type { RecordModel } from 'pocketbase';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { paths } from '@/paths'; import { paths } from '@/paths';
@@ -39,14 +39,12 @@ import { PropertyItem } from '@/components/core/property-item';
import { PropertyList } from '@/components/core/property-list'; import { PropertyList } from '@/components/core/property-list';
import { toast } from '@/components/core/toaster'; import { toast } from '@/components/core/toaster';
import ErrorDisplay from '@/components/dashboard/error'; import ErrorDisplay from '@/components/dashboard/error';
import { defaultLessonCategory } from '@/components/dashboard/lesson_category/_constants.ts'; import { defaultVocabulary } from '@/components/dashboard/vocabulary/_constants.ts';
// import { defaultLessonCategory } from '@/components/dashboard/lesson_category/defaultLessonCategory'; import { Notifications } from '@/components/dashboard/vocabulary/notifications';
import { Notifications } from '@/components/dashboard/lesson_category/notifications'; import { Payments } from '@/components/dashboard/vocabulary/payments';
import { Payments } from '@/components/dashboard/lesson_category/payments'; import type { Address } from '@/components/dashboard/vocabulary/shipping-address';
import type { Address } from '@/components/dashboard/lesson_category/shipping-address'; import { ShippingAddress } from '@/components/dashboard/vocabulary/shipping-address';
import { ShippingAddress } from '@/components/dashboard/lesson_category/shipping-address'; import type { Vocabulary } from '@/components/dashboard/vocabulary/type';
import { LessonCategory } from '@/components/dashboard/lesson_category/type';
// import type { LessonCategory } from '@/components/dashboard/lp_categories/type';
import FormLoading from '@/components/loading'; import FormLoading from '@/components/loading';
// export const metadata = { title: `Details | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; // export const metadata = { title: `Details | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;
@@ -60,18 +58,18 @@ export default function Page(): React.JSX.Element {
const [showLoading, setShowLoading] = React.useState<boolean>(true); const [showLoading, setShowLoading] = React.useState<boolean>(true);
const [showError, setShowError] = React.useState<boolean>(false); const [showError, setShowError] = React.useState<boolean>(false);
// //
const [showLessonCategory, setShowLessonCategory] = React.useState<LessonCategory>(defaultLessonCategory); const [showLessonCategory, setShowLessonCategory] = React.useState<Vocabulary>(defaultVocabulary);
function handleEditClick() { function handleEditClick() {
router.push(paths.dashboard.lesson_categories.edit(showLessonCategory.id)); router.push(paths.dashboard.vocabularies.edit(showLessonCategory.id));
} }
React.useEffect(() => { React.useEffect(() => {
if (catId) { if (catId) {
pb.collection(COL_LESSON_CATEGORIES) pb.collection(COL_VOCABULARIES)
.getOne(catId) .getOne(catId)
.then((model: RecordModel) => { .then((model: RecordModel) => {
setShowLessonCategory({ ...defaultLessonCategory, ...model }); setShowLessonCategory({ ...defaultVocabulary, ...model });
}) })
.catch((err) => { .catch((err) => {
logger.error(err); logger.error(err);
@@ -111,7 +109,7 @@ export default function Page(): React.JSX.Element {
<Link <Link
color="text.primary" color="text.primary"
component={RouterLink} component={RouterLink}
href={paths.dashboard.lesson_categories.list} href={paths.dashboard.vocabularies.list}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }} sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2" variant="subtitle2"
> >
@@ -119,8 +117,16 @@ export default function Page(): React.JSX.Element {
{t('dashboard.lessonCategorys.list.title')} {t('dashboard.lessonCategorys.list.title')}
</Link> </Link>
</div> </div>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}> <Stack
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto' }}> direction={{ xs: 'column', sm: 'row' }}
spacing={3}
sx={{ alignItems: 'flex-start' }}
>
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center', flex: '1 1 auto' }}
>
<Avatar <Avatar
src={`http://127.0.0.1:8090/api/files/${showLessonCategory.collectionId}/${showLessonCategory.id}/${showLessonCategory.cat_image}`} src={`http://127.0.0.1:8090/api/files/${showLessonCategory.collectionId}/${showLessonCategory.id}/${showLessonCategory.cat_image}`}
sx={{ '--Avatar-size': '64px' }} sx={{ '--Avatar-size': '64px' }}
@@ -129,29 +135,50 @@ export default function Page(): React.JSX.Element {
empty empty
</Avatar> </Avatar>
<div> <div>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap' }}> <Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
>
<Typography variant="h4">{showLessonCategory.name}</Typography> <Typography variant="h4">{showLessonCategory.name}</Typography>
<Chip <Chip
icon={<CheckCircleIcon color="var(--mui-palette-success-main)" weight="fill" />} icon={
<CheckCircleIcon
color="var(--mui-palette-success-main)"
weight="fill"
/>
}
label={showLessonCategory.visible} label={showLessonCategory.visible}
size="small" size="small"
variant="outlined" variant="outlined"
/> />
</Stack> </Stack>
<Typography color="text.secondary" variant="body1"> <Typography
color="text.secondary"
variant="body1"
>
{showLessonCategory.id} {showLessonCategory.id}
</Typography> </Typography>
</div> </div>
</Stack> </Stack>
<div> <div>
<Button endIcon={<CaretDownIcon />} variant="contained"> <Button
endIcon={<CaretDownIcon />}
variant="contained"
>
Action Action
</Button> </Button>
</div> </div>
</Stack> </Stack>
</Stack> </Stack>
<Grid container spacing={4}> <Grid
<Grid lg={4} xs={12}> container
spacing={4}
>
<Grid
lg={4}
xs={12}
>
<Stack spacing={4}> <Stack spacing={4}>
<Card> <Card>
<CardHeader <CardHeader
@@ -169,7 +196,7 @@ export default function Page(): React.JSX.Element {
<UserIcon fontSize="var(--Icon-fontSize)" /> <UserIcon fontSize="var(--Icon-fontSize)" />
</Avatar> </Avatar>
} }
title={t('basic-details', { ns: 'lesson_category' })} title={t('basic-details')}
/> />
<PropertyList <PropertyList
divider={<Divider />} divider={<Divider />}
@@ -178,8 +205,17 @@ export default function Page(): React.JSX.Element {
> >
{( {(
[ [
{ key: 'Customer ID', value: <Chip label={showLessonCategory.id} size="small" variant="soft" /> }, {
{ key: 'Name', value: showLessonCategory.name }, key: 'word',
value: (
<Chip
label={showLessonCategory.word}
size="small"
variant="soft"
/>
),
},
{ key: 'word_c', value: showLessonCategory.word_c },
{ key: 'Pos', value: showLessonCategory.pos }, { key: 'Pos', value: showLessonCategory.pos },
{ {
key: 'Visible', key: 'Visible',
@@ -195,9 +231,20 @@ export default function Page(): React.JSX.Element {
{ {
key: 'Quota', key: 'Quota',
value: ( value: (
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}> <Stack
<LinearProgress sx={{ flex: '1 1 auto' }} value={50} variant="determinate" /> direction="row"
<Typography color="text.secondary" variant="body2"> spacing={2}
sx={{ alignItems: 'center' }}
>
<LinearProgress
sx={{ flex: '1 1 auto' }}
value={50}
variant="determinate"
/>
<Typography
color="text.secondary"
variant="body2"
>
50% 50%
</Typography> </Typography>
</Stack> </Stack>
@@ -206,7 +253,11 @@ export default function Page(): React.JSX.Element {
] satisfies { key: string; value: React.ReactNode }[] ] satisfies { key: string; value: React.ReactNode }[]
).map( ).map(
(item): React.JSX.Element => ( (item): React.JSX.Element => (
<PropertyItem key={item.key} name={item.key} value={item.value} /> <PropertyItem
key={item.key}
name={item.key}
value={item.value}
/>
) )
)} )}
</PropertyList> </PropertyList>
@@ -218,16 +269,22 @@ export default function Page(): React.JSX.Element {
<ShieldWarningIcon fontSize="var(--Icon-fontSize)" /> <ShieldWarningIcon fontSize="var(--Icon-fontSize)" />
</Avatar> </Avatar>
} }
title={t('security', { ns: 'lesson_category' })} title={t('security')}
/> />
<CardContent> <CardContent>
<Stack spacing={1}> <Stack spacing={1}>
<div> <div>
<Button color="error" variant="contained"> <Button
color="error"
variant="contained"
>
Delete account Delete account
</Button> </Button>
</div> </div>
<Typography color="text.secondary" variant="body2"> <Typography
color="text.secondary"
variant="body2"
>
A deleted lesson category cannot be restored. All data will be permanently removed. A deleted lesson category cannot be restored. All data will be permanently removed.
</Typography> </Typography>
</Stack> </Stack>
@@ -235,7 +292,10 @@ export default function Page(): React.JSX.Element {
</Card> </Card>
</Stack> </Stack>
</Grid> </Grid>
<Grid lg={8} xs={12}> <Grid
lg={8}
xs={12}
>
<Stack spacing={4}> <Stack spacing={4}>
<Payments <Payments
ordersValue={2069.48} ordersValue={2069.48}
@@ -282,7 +342,10 @@ export default function Page(): React.JSX.Element {
<Card> <Card>
<CardHeader <CardHeader
action={ action={
<Button color="secondary" startIcon={<PencilSimpleIcon />}> <Button
color="secondary"
startIcon={<PencilSimpleIcon />}
>
Edit Edit
</Button> </Button>
} }
@@ -291,11 +354,17 @@ export default function Page(): React.JSX.Element {
<CreditCardIcon fontSize="var(--Icon-fontSize)" /> <CreditCardIcon fontSize="var(--Icon-fontSize)" />
</Avatar> </Avatar>
} }
title={t('billing-details', { ns: 'lesson_category' })} title={t('billing-details')}
/> />
<CardContent> <CardContent>
<Card sx={{ borderRadius: 1 }} variant="outlined"> <Card
<PropertyList divider={<Divider />} sx={{ '--PropertyItem-padding': '16px' }}> sx={{ borderRadius: 1 }}
variant="outlined"
>
<PropertyList
divider={<Divider />}
sx={{ '--PropertyItem-padding': '16px' }}
>
{( {(
[ [
{ key: 'Credit card', value: '**** 4142' }, { key: 'Credit card', value: '**** 4142' },
@@ -307,7 +376,11 @@ export default function Page(): React.JSX.Element {
] satisfies { key: string; value: React.ReactNode }[] ] satisfies { key: string; value: React.ReactNode }[]
).map( ).map(
(item): React.JSX.Element => ( (item): React.JSX.Element => (
<PropertyItem key={item.key} name={item.key} value={item.value} /> <PropertyItem
key={item.key}
name={item.key}
value={item.value}
/>
) )
)} )}
</PropertyList> </PropertyList>
@@ -317,7 +390,10 @@ export default function Page(): React.JSX.Element {
<Card> <Card>
<CardHeader <CardHeader
action={ action={
<Button color="secondary" startIcon={<PlusIcon />}> <Button
color="secondary"
startIcon={<PlusIcon />}
>
Add Add
</Button> </Button>
} }
@@ -326,10 +402,13 @@ export default function Page(): React.JSX.Element {
<HouseIcon fontSize="var(--Icon-fontSize)" /> <HouseIcon fontSize="var(--Icon-fontSize)" />
</Avatar> </Avatar>
} }
title={t('shipping-addresses', { ns: 'lesson_category' })} title={t('shipping-addresses')}
/> />
<CardContent> <CardContent>
<Grid container spacing={3}> <Grid
container
spacing={3}
>
{( {(
[ [
{ {
@@ -351,7 +430,11 @@ export default function Page(): React.JSX.Element {
}, },
] satisfies Address[] ] satisfies Address[]
).map((address) => ( ).map((address) => (
<Grid key={address.id} md={6} xs={12}> <Grid
key={address.id}
md={6}
xs={12}
>
<ShippingAddress address={address} /> <ShippingAddress address={address} />
</Grid> </Grid>
))} ))}

View File

@@ -127,7 +127,7 @@ export function CrCategoryEditForm(): React.JSX.Element {
// TODO: remove below // TODO: remove below
type: '', type: '',
}; };
//
try { try {
const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).update(catId, tempUpdate); const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).update(catId, tempUpdate);
logger.debug(result); logger.debug(result);

View File

@@ -28,27 +28,27 @@ export const layoutConfig = {
items: [ items: [
{ {
key: 'lesson-types', key: 'lesson-types',
title: 'type', title: 'lesson-types',
icon: 'users', icon: 'users',
items: [ items: [
{ {
key: 'lesson-types', key: 'lesson-types',
title: 'dashboard.lessonTypes.list', title: 'lessonTypes.list',
href: paths.dashboard.lesson_types.list, href: paths.dashboard.lesson_types.list,
}, },
{ {
key: 'lesson-categories', key: 'lesson-categories',
title: 'dashboard.lessonCategories.list', title: 'lessonCategories.list',
href: paths.dashboard.lesson_categories.list, href: paths.dashboard.lesson_categories.list,
}, },
{ {
key: 'vocabulary', key: 'vocabulary',
title: 'dashboard.vocabularies.list', title: 'vocabularies.list',
href: paths.dashboard.vocabularies.list, href: paths.dashboard.vocabularies.list,
}, },
{ {
key: 'connective', key: 'connective',
title: 'dashboard.connectives.list', title: 'connectives.list',
href: paths.dashboard.connectives.list, href: paths.dashboard.connectives.list,
}, },
], ],

View File

@@ -1,3 +1,5 @@
// RULES: obsoleted
// please use `./cms/src/db/LessonCategories/type.d.ts`
export interface LessonCategory { export interface LessonCategory {
isEmpty?: boolean; isEmpty?: boolean;
// //

View File

@@ -127,7 +127,7 @@ export function MfCategoryEditForm(): React.JSX.Element {
// TODO: remove below // TODO: remove below
type: '', type: '',
}; };
//
try { try {
const result = await pb.collection(COL_QUIZ_MF_CATEGORIES).update(catId, tempUpdate); const result = await pb.collection(COL_QUIZ_MF_CATEGORIES).update(catId, tempUpdate);
logger.debug(result); logger.debug(result);

View File

@@ -1,7 +1,9 @@
'use client'; 'use client';
// RULES: sorting direction for student lists
export type SortDir = 'asc' | 'desc'; export type SortDir = 'asc' | 'desc';
// RULES: core student data structure
export interface Student { export interface Student {
id: string; id: string;
name: string; name: string;
@@ -14,6 +16,7 @@ export interface Student {
updatedAt?: Date; updatedAt?: Date;
} }
// RULES: form data structure for creating new student
export interface CreateFormProps { export interface CreateFormProps {
name: string; name: string;
email: string; email: string;
@@ -36,6 +39,7 @@ export interface CreateFormProps {
// status?: 'pending' | 'active' | 'blocked'; // status?: 'pending' | 'active' | 'blocked';
} }
// RULES: form data structure for editing existing student
export interface EditFormProps { export interface EditFormProps {
name: string; name: string;
email: string; email: string;
@@ -57,11 +61,14 @@ export interface EditFormProps {
// quota?: number; // quota?: number;
// status?: 'pending' | 'active' | 'blocked'; // status?: 'pending' | 'active' | 'blocked';
} }
// RULES: filter props for student search and filtering
export interface CustomersFiltersProps { export interface CustomersFiltersProps {
filters?: Filters; filters?: Filters;
sortDir?: SortDir; sortDir?: SortDir;
fullData: Student[]; fullData: Student[];
} }
// RULES: available filter options for student data
export interface Filters { export interface Filters {
email?: string; email?: string;
phone?: string; phone?: string;

View File

@@ -1,7 +1,9 @@
'use client'; 'use client';
// RULES: sorting direction for teacher lists
export type SortDir = 'asc' | 'desc'; export type SortDir = 'asc' | 'desc';
// RULES: core teacher data structure
export interface Teacher { export interface Teacher {
id: string; id: string;
name: string; name: string;
@@ -14,6 +16,7 @@ export interface Teacher {
updatedAt?: Date; updatedAt?: Date;
} }
// RULES: form data structure for creating new teacher
export interface CreateFormProps { export interface CreateFormProps {
name: string; name: string;
email: string; email: string;
@@ -36,6 +39,7 @@ export interface CreateFormProps {
// status?: 'pending' | 'active' | 'blocked'; // status?: 'pending' | 'active' | 'blocked';
} }
// RULES: form data structure for editing existing teacher
export interface EditFormProps { export interface EditFormProps {
name: string; name: string;
email: string; email: string;
@@ -57,6 +61,8 @@ export interface EditFormProps {
// quota?: number; // quota?: number;
// status?: 'pending' | 'active' | 'blocked'; // status?: 'pending' | 'active' | 'blocked';
} }
// RULES: filter props for teacher search and filtering
export interface CustomersFiltersProps { export interface CustomersFiltersProps {
filters?: Filters; filters?: Filters;
sortDir?: SortDir; sortDir?: SortDir;

View File

@@ -1,44 +1,40 @@
import { dayjs } from '@/lib/dayjs'; import { dayjs } from '@/lib/dayjs';
import { Vocabulary, CreateForm } from './type';
import { CreateForm, LessonCategory } from './type'; export const defaultVocabulary: Vocabulary = {
id: 'default-vocabulary-id',
// import type { CreateForm, LessonCategory } from '../lp_categories/type'; image: undefined,
sound: undefined,
export const defaultLessonCategory: LessonCategory = { word: '',
isEmpty: false, word_c: '',
id: 'default-id', sample_e: '',
cat_name: 'default-category-name', sample_c: '',
cat_image_url: undefined, cat_id: 'default-category-id',
cat_image: undefined, category: 'default-category',
pos: 0, lesson_type_id: 'default-lesson-type-id',
visible: 'hidden',
lesson_id: 'default-lesson-id',
description: 'default-description',
remarks: 'default-remarks',
//
collectionId: '0000000000',
createdAt: dayjs('2099-01-01').toDate(),
//
name: '',
avatar: '',
email: '',
phone: '',
quota: 0,
status: 'NA',
}; };
export const LessonCategoryCreateFormDefault: CreateForm = { export const VocabularyCreateFormDefault: CreateForm = {
name: '', image: undefined,
type: '', sound: undefined,
pos: 1, word: '',
visible: 'visible', word_c: '',
description: '', sample_e: '',
isActive: true, sample_c: '',
order: 1, cat_id: '',
imageUrl: '', category: '',
lesson_type_id: '',
}; };
export const emptyLessonCategory: LessonCategory = { export const emptyVocabulary: Vocabulary = {
...defaultLessonCategory, ...defaultVocabulary,
id: '',
word: '',
word_c: '',
sample_e: '',
sample_c: '',
cat_id: '',
category: '',
lesson_type_id: '',
isEmpty: true, isEmpty: true,
}; };

View File

@@ -15,6 +15,7 @@ import { useTranslation } from 'react-i18next';
import { logger } from '@/lib/default-logger'; import { logger } from '@/lib/default-logger';
import { toast } from '@/components/core/toaster'; import { toast } from '@/components/core/toaster';
import deleteVocabulary from '@/db/Vocabularies/Delete';
const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL); const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL);
@@ -44,33 +45,24 @@ export default function ConfirmDeleteModal({
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
}; };
function performDelete(id: string): Promise<void> {
return pb
.collection(COL_LESSON_TYPES)
.delete(id)
.then(() => {
toast(t('dashboard.lessonTypes.delete.success'));
reloadRows();
})
.catch((err) => {
logger.error(err);
toast(t('dashboard.lessonTypes.delete.error'));
})
.finally(() => {});
}
function handleUserConfirmDelete(): void { function handleUserConfirmDelete(): void {
if (idToDelete) { if (idToDelete) {
setIsDeleteing(true); setIsDeleteing(true);
performDelete(idToDelete)
// RULES: Vocabulary -> deleteVocabulary
deleteVocabulary(idToDelete)
.then(() => { .then(() => {
reloadRows();
handleClose(); handleClose();
setIsDeleteing(false); toast(t('delete.success'));
}) })
.catch((err) => { .catch((err) => {
// console.error(err) // console.error(err)
logger.error(err); logger.error(err);
toast(t('dashboard.lessonTypes.delete.error')); toast(t('delete.error'));
})
.finally(() => {
setIsDeleteing(false);
}); });
} }
} }
@@ -86,19 +78,33 @@ export default function ConfirmDeleteModal({
<Box sx={style}> <Box sx={style}>
<Container maxWidth="sm"> <Container maxWidth="sm">
<Paper sx={{ border: '1px solid var(--mui-palette-divider)', boxShadow: 'var(--mui-shadows-16)' }}> <Paper sx={{ border: '1px solid var(--mui-palette-divider)', boxShadow: 'var(--mui-shadows-16)' }}>
<Stack direction="row" spacing={2} sx={{ display: 'flex', p: 3 }}> <Stack
direction="row"
spacing={2}
sx={{ display: 'flex', p: 3 }}
>
<Avatar sx={{ bgcolor: 'var(--mui-palette-error-50)', color: 'var(--mui-palette-error-main)' }}> <Avatar sx={{ bgcolor: 'var(--mui-palette-error-50)', color: 'var(--mui-palette-error-main)' }}>
<NoteIcon fontSize="var(--Icon-fontSize)" /> <NoteIcon fontSize="var(--Icon-fontSize)" />
</Avatar> </Avatar>
<Stack spacing={3}> <Stack spacing={3}>
<Stack spacing={1}> <Stack spacing={1}>
<Typography variant="h5">{t('Delete Lesson Type ?')}</Typography> <Typography variant="h5">{t('Delete Lesson Type ?')}</Typography>
<Typography color="text.secondary" variant="body2"> <Typography
color="text.secondary"
variant="body2"
>
{t('Are you sure you want to delete lesson type ?')} {t('Are you sure you want to delete lesson type ?')}
</Typography> </Typography>
</Stack> </Stack>
<Stack direction="row" spacing={2} sx={{ justifyContent: 'flex-end' }}> <Stack
<Button color="secondary" onClick={handleClose}> direction="row"
spacing={2}
sx={{ justifyContent: 'flex-end' }}
>
<Button
color="secondary"
onClick={handleClose}
>
{t('Cancel')} {t('Cancel')}
</Button> </Button>
<LoadingButton <LoadingButton

View File

@@ -1,64 +0,0 @@
import { dayjs } from '@/lib/dayjs';
export interface LessonCategory {
isEmpty?: boolean;
//
id: string;
cat_name: string;
cat_image_url?: string;
cat_image?: string;
pos: number;
visible: string;
lesson_id: string;
description: string;
remarks: string;
//
name: string;
avatar: string;
email: string;
phone: string;
quota: number;
status: 'pending' | 'active' | 'blocked' | 'NA';
createdAt: Date;
}
export const defaultLessonCategory: LessonCategory = {
isEmpty: false,
id: 'default-id',
cat_name: 'default-category-name',
cat_image_url: undefined,
cat_image: undefined,
pos: 0,
visible: 'hidden',
lesson_id: 'default-lesson-id',
description: 'default-description',
remarks: 'default-remarks',
//
createdAt: dayjs('2099-01-01').toDate(),
//
name: '',
avatar: '',
email: '',
phone: '',
quota: 0,
status: 'NA',
};
export const emptyLessonCategory: LessonCategory = {
...defaultLessonCategory,
isEmpty: true,
};
export interface CreateForm {
name: string;
type: string;
pos: number;
visible: string;
}
export const LessonCategoryCreateFormDefault: CreateForm = {
name: '',
type: '',
pos: 1,
visible: 'visible',
};

View File

@@ -1,53 +0,0 @@
'use client';
import * as React from 'react';
// import type { LessonCategory } from '@/types/lesson-type';
import { useSelection } from '@/hooks/use-selection';
import type { Selection } from '@/hooks/use-selection';
import { LessonCategory } from './type';
// import type { LessonCategory } from '../lp_categories/type';
// import type { LessonCategory } from './lesson-categories-table';
// import type { LessonCategory } from '@/components/dashboard/lesson_category/interfaces';
function noop(): void {
return undefined;
}
export interface LessonCategoriesSelectionContextValue extends Selection {}
export const LessonCategoriesSelectionContext = React.createContext<LessonCategoriesSelectionContextValue>({
deselectAll: noop,
deselectOne: noop,
selectAll: noop,
selectOne: noop,
selected: new Set(),
selectedAny: false,
selectedAll: false,
});
interface LessonCategoriesSelectionProviderProps {
children: React.ReactNode;
lessonCategories: LessonCategory[];
}
export function LessonCategoriesSelectionProvider({
children,
lessonCategories = [],
}: LessonCategoriesSelectionProviderProps): React.JSX.Element {
const customerIds = React.useMemo(() => lessonCategories.map((customer) => customer.id), [lessonCategories]);
const selection = useSelection(customerIds);
return (
<LessonCategoriesSelectionContext.Provider value={{ ...selection }}>
{children}
</LessonCategoriesSelectionContext.Provider>
);
}
export function useLessonCategoriesSelection(): LessonCategoriesSelectionContextValue {
return React.useContext(LessonCategoriesSelectionContext);
}

View File

@@ -1,353 +0,0 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import { COL_LESSON_CATEGORIES, NS_LESSON_CATEGORY } from '@/constants';
import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab';
import { Avatar, Divider, MenuItem } from '@mui/material';
// import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardActions from '@mui/material/CardActions';
import CardContent from '@mui/material/CardContent';
// import Checkbox from '@mui/material/Checkbox';
import FormControl from '@mui/material/FormControl';
// import FormControlLabel from '@mui/material/FormControlLabel';
import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel';
import OutlinedInput from '@mui/material/OutlinedInput';
import Select from '@mui/material/Select';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Unstable_Grid2';
import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera';
import type { RecordModel } from 'pocketbase';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import { dayjs } from '@/lib/dayjs';
import { logger } from '@/lib/default-logger';
import { fileToBase64 } from '@/lib/file-to-base64';
import { pb } from '@/lib/pb';
import { Option } from '@/components/core/option';
import { TextEditor } from '@/components/core/text-editor/text-editor';
import { toast } from '@/components/core/toaster';
import FormLoading from '@/components/loading';
import ErrorDisplay from '../error';
import { defaultLessonCategory } from './_constants';
import type { EditFormProps, LessonCategory } from './type';
// TODO: review this
const schema = zod.object({
cat_name: zod.string().min(1, 'name-is-required').max(255),
//
pos: zod.number().min(1, 'Phone is required').max(99),
visible: zod.string().max(255),
//
description: zod.string().optional(),
remarks: zod.string().optional(),
});
type Values = zod.infer<typeof schema>;
const defaultValues = {
cat_name: '',
// cat_image: undefined,
pos: 0,
visible: 'hidden',
// lesson_id: 'default-lesson-id',
description: 'default-description',
remarks: 'default-remarks',
//
} satisfies Values;
export function LessonCategoryEditForm(): React.JSX.Element {
const router = useRouter();
const { t } = useTranslation(['common', 'lesson_category']);
const NS_DEFAULT = { ns: 'lesson_category' };
const { cat_id: catId } = useParams<{ cat_id: string }>();
//
const [isUpdating, setIsUpdating] = React.useState<boolean>(false);
const [showLoading, setShowLoading] = React.useState<boolean>(false);
const [showError, setShowError] = React.useState<boolean>(false);
const {
control,
handleSubmit,
formState: { errors },
setValue,
reset,
watch,
} = useForm<Values>({ defaultValues, resolver: zodResolver(schema) });
const onSubmit = React.useCallback(async (values: Values): Promise<void> => {
setIsUpdating(true);
const tempUpdate: EditFormProps = {
cat_name: values.cat_name,
pos: values.pos,
visible: values.visible,
description: values.description,
remarks: values.remarks,
type: '',
};
pb.collection(COL_LESSON_CATEGORIES)
.update(catId, tempUpdate)
.then((res) => {
logger.debug(res);
toast.success(t('update.success', NS_DEFAULT));
router.push(paths.dashboard.lesson_categories.list);
})
.catch((err) => {
logger.error(err);
toast.error('Something went wrong!');
})
.finally(() => {
//
setIsUpdating(false);
});
}, []);
const avatarInputRef = React.useRef<HTMLInputElement>(null);
// const avatar = watch('avatar');
const handleAvatarChange = React.useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const url = await fileToBase64(file);
// setValue('avatar', url);
}
},
[setValue]
);
const [textDescription, setTextDescription] = React.useState<string>('loading');
const [textRemarks, setTextRemarks] = React.useState<string>('loading');
const handleLoad = React.useCallback(
(id: string) => {
setShowLoading(true);
pb.collection(COL_LESSON_CATEGORIES)
.getOne(id)
.then((model: RecordModel) => {
const temp: LessonCategory = { ...defaultLessonCategory, ...model };
reset(temp);
setTextDescription(temp.description);
setTextRemarks(temp.remarks);
})
.catch((err) => {
logger.error(err);
toast(t('list.error', NS_DEFAULT));
})
.finally(() => {
setShowLoading(false);
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[catId]
);
React.useEffect(() => {
handleLoad(catId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [catId]);
if (showLoading) return <FormLoading />;
if (showError)
return (
<ErrorDisplay
message={t('error.unable-to-process-request', NS_DEFAULT)}
code="500"
details={t('error.detailed-error-information', NS_DEFAULT)}
/>
);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Card>
<CardContent>
<Stack divider={<Divider />} spacing={4}>
<Stack spacing={3}>
<Typography variant="h6">{t('edit.basic-info', NS_DEFAULT)}</Typography>
<Grid container spacing={3}>
<Grid xs={12}>
<Stack direction="row" spacing={3} sx={{ alignItems: 'center' }}>
<Box
sx={{
border: '1px dashed var(--mui-palette-divider)',
borderRadius: '5%',
display: 'inline-flex',
p: '4px',
}}
>
{/*
<Avatar
src={avatar}
sx={{
'--Avatar-size': '100px',
'--Icon-fontSize': 'var(--icon-fontSize-lg)',
alignItems: 'center',
bgcolor: 'var(--mui-palette-background-level1)',
color: 'var(--mui-palette-text-primary)',
display: 'flex',
justifyContent: 'center',
}}
>
// TODO: resume me
<CameraIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
*/}
</Box>
<Stack spacing={1} sx={{ alignItems: 'flex-start' }}>
<Typography variant="subtitle1">{t('edit.avatar', NS_DEFAULT)}</Typography>
<Typography variant="caption">{t('edit.avatarRequirements', NS_DEFAULT)}</Typography>
<Button
color="secondary"
onClick={() => {
avatarInputRef.current?.click();
}}
variant="outlined"
>
{t('edit.select', NS_DEFAULT)}
</Button>
<input hidden onChange={handleAvatarChange} ref={avatarInputRef} type="file" />
</Stack>
</Stack>
</Grid>
<Grid md={6} xs={12}>
<Controller
disabled={isUpdating}
control={control}
name="cat_name"
render={({ field }) => (
<FormControl disabled={isUpdating} error={Boolean(errors.cat_name)} fullWidth>
<InputLabel required>{t('edit.name', NS_DEFAULT)}</InputLabel>
<OutlinedInput {...field} />
{errors.cat_name ? <FormHelperText>{errors.cat_name.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
<Grid md={6} xs={12}>
<Controller
disabled={isUpdating}
control={control}
name="pos"
render={({ field }) => (
<FormControl error={Boolean(errors.pos)} fullWidth>
<InputLabel required>{t('edit.position', NS_DEFAULT)}</InputLabel>
<OutlinedInput
{...field}
onChange={(e) => {
field.onChange(parseInt(e.target.value));
}}
type="number"
/>
{errors.pos ? <FormHelperText>{errors.pos.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
<Grid md={6} xs={12}>
<Controller
disabled={isUpdating}
control={control}
name="visible"
render={({ field }) => (
<FormControl error={Boolean(errors.visible)} fullWidth>
<InputLabel>{t('edit.visible', NS_DEFAULT)}</InputLabel>
<Select {...field}>
<MenuItem value="visible">{t('edit.visible', NS_DEFAULT)}</MenuItem>
<MenuItem value="hidden">{t('edit.hidden', NS_DEFAULT)}</MenuItem>
</Select>
{errors.visible ? <FormHelperText>{errors.visible.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
</Grid>
</Stack>
<Stack spacing={3}>
<Typography variant="h6">{t('create.detail-information', NS_DEFAULT)}</Typography>
<Grid container spacing={3}>
<Grid md={6} xs={12}>
<Controller
disabled={isUpdating}
control={control}
name="description"
render={({ field }) => {
return (
<Box>
<Typography variant="subtitle1" color="text-secondary">
{t('create.description', NS_DEFAULT)}
</Typography>
<Box sx={{ mt: '8px', '& .tiptap-container': { height: '400px' } }}>
<TextEditor
{...field}
content={textDescription}
onUpdate={({ editor }) => {
field.onChange({ target: { value: editor.getHTML() } });
}}
placeholder={t('edit.write-something', NS_DEFAULT)}
/>
</Box>
</Box>
);
}}
/>
</Grid>
<Grid md={6} xs={12}>
<Controller
disabled={isUpdating}
control={control}
name="remarks"
render={({ field }) => (
<Box>
<Typography variant="subtitle1" color="text.secondary">
{t('create.remarks', NS_DEFAULT)}
</Typography>
<Box sx={{ mt: '8px', '& .tiptap-container': { height: '400px' } }}>
<TextEditor
content={textRemarks}
onUpdate={({ editor }) => {
field.onChange({ target: { value: editor.getText() } });
}}
hideToolbar
placeholder={t('edit.write-something', NS_DEFAULT)}
/>
</Box>
</Box>
)}
/>
</Grid>
</Grid>
</Stack>
</Stack>
</CardContent>
<CardActions sx={{ justifyContent: 'flex-end' }}>
<Button color="secondary" component={RouterLink} href={paths.dashboard.lesson_types.list}>
{t('edit.cancelButton', NS_DEFAULT)}
</Button>
<LoadingButton disabled={isUpdating} loading={isUpdating} type="submit" variant="contained">
{t('edit.updateButton', NS_DEFAULT)}
</LoadingButton>
</CardActions>
</Card>
</form>
);
}

View File

@@ -0,0 +1,36 @@
export interface Vocabulary {
id: string;
created?: string;
updated?: string;
image?: string;
sound?: string;
word?: string;
word_c?: string;
sample_e?: string;
sample_c?: string;
cat_id?: string;
category?: string;
lesson_type_id?: string;
}
export interface CreateForm {
image?: string;
sound?: string;
word?: string;
word_c?: string;
sample_e?: string;
sample_c?: string;
cat_id?: string;
category?: string;
lesson_type_id?: string;
}
export interface EditFormProps {
id: string;
defaultValues: Vocabulary;
onDone: () => void;
}
export interface Helloworld {
helloworld: string;
}

View File

@@ -1,47 +1,60 @@
export interface LessonCategory { // RULES:
isEmpty?: boolean; // should match the collection `Vocabularies` from `schema.dbml`
// export interface Vocabulary {
id: string; id: string;
created?: string;
updated?: string;
image?: string;
sound?: string;
word?: string;
word_c?: string;
sample_e?: string;
sample_c?: string;
cat_id: string;
category?: string;
lesson_type_id?: string;
visible: string;
expand?: {
cat_id?: {
collectionId: string; collectionId: string;
// id: string;
cat_image: string;
cat_name: string; cat_name: string;
cat_image_url?: string;
cat_image?: string;
pos: number;
visible: string;
lesson_id: string;
description: string;
remarks: string;
createdAt: Date;
// //
name: string; };
avatar: string; };
email: string;
phone: string;
quota: number;
status: 'pending' | 'active' | 'blocked' | 'NA';
} }
// RULES: for use with vocabulary-create-form.tsx
// when you update, please take a look into `vocabulary-create-form.tsx`
export interface CreateForm { export interface CreateForm {
name: string; image?: string;
type: string; sound?: string;
pos: number; word?: string;
visible: string; word_c?: string;
description: string; sample_e?: string;
isActive: boolean; sample_c?: string;
order: number; cat_id?: string;
imageUrl: string; category?: string;
lesson_type_id?: string;
} }
// RULES: for use with vocabulary-edit-form.tsx
// when you update, please take a look into `vocabulary-edit-form.tsx`
export interface EditFormProps { export interface EditFormProps {
cat_name: string; image: File[] | null;
pos: number; sound: string;
visible: string; word: string;
description?: string; word_c: string;
remarks?: string; sample_e: string;
type: string; sample_c: string;
cat_id: string;
category: string;
lesson_type_id: string;
} }
// RULES:
// helloworld example, should not modify
export interface Helloworld { export interface Helloworld {
helloworld: string; helloworld: string;
} }

View File

@@ -2,7 +2,6 @@
import * as React from 'react'; import * as React from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { COL_LESSON_CATEGORIES } from '@/constants';
import GetAllCount from '@/db/LessonCategories/GetAllCount'; import GetAllCount from '@/db/LessonCategories/GetAllCount';
import GetHiddenCount from '@/db/LessonCategories/GetHiddenCount'; import GetHiddenCount from '@/db/LessonCategories/GetHiddenCount';
import GetVisibleCount from '@/db/LessonCategories/GetVisibleCount'; import GetVisibleCount from '@/db/LessonCategories/GetVisibleCount';
@@ -20,13 +19,11 @@ import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { paths } from '@/paths'; import { paths } from '@/paths';
import { pb } from '@/lib/pb';
import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button';
import { Option } from '@/components/core/option'; import { Option } from '@/components/core/option';
// import { LessonCategory } from '../lp_categories/type'; import { useVocabulariesSelection } from './vocabularies-selection-context';
import { useLessonCategoriesSelection } from './lesson-categories-selection-context'; import type { Vocabulary } from './type';
import { LessonCategory } from './type';
export interface Filters { export interface Filters {
email?: string; email?: string;
@@ -39,17 +36,17 @@ export interface Filters {
export type SortDir = 'asc' | 'desc'; export type SortDir = 'asc' | 'desc';
export interface LessonCategoriesFiltersProps { export interface VocabulariesFiltersProps {
filters?: Filters; filters?: Filters;
sortDir?: SortDir; sortDir?: SortDir;
fullData: LessonCategory[]; fullData: Vocabulary[];
} }
export function LessonCategoriesFilters({ export function VocabulariesFilters({
filters = {}, filters = {},
sortDir = 'desc', sortDir = 'desc',
fullData, fullData,
}: LessonCategoriesFiltersProps): React.JSX.Element { }: VocabulariesFiltersProps): React.JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
const { email, phone, status, name, visible, type } = filters; const { email, phone, status, name, visible, type } = filters;
@@ -59,16 +56,16 @@ export function LessonCategoriesFilters({
const router = useRouter(); const router = useRouter();
const selection = useLessonCategoriesSelection(); const selection = useVocabulariesSelection();
function getVisible(): number { function getVisible(): number {
return fullData.reduce((count, item: LessonCategory) => { return fullData.reduce((count, item: Vocabulary) => {
return item.visible === 'visible' ? count + 1 : count; return item.visible === 'visible' ? count + 1 : count;
}, 0); }, 0);
} }
function getHidden(): number { function getHidden(): number {
return fullData.reduce((count, item: LessonCategory) => { return fullData.reduce((count, item: Vocabulary) => {
return item.visible === 'hidden' ? count + 1 : count; return item.visible === 'hidden' ? count + 1 : count;
}, 0); }, 0);
} }
@@ -115,7 +112,7 @@ export function LessonCategoriesFilters({
searchParams.set('visible', newFilters.visible); searchParams.set('visible', newFilters.visible);
} }
router.push(`${paths.dashboard.lesson_categories.list}?${searchParams.toString()}`); router.push(`${paths.dashboard.vocabularies.list}?${searchParams.toString()}`);
}, },
[router] [router]
); );
@@ -195,10 +192,21 @@ export function LessonCategoriesFilters({
return ( return (
<div> <div>
<Tabs onChange={handleVisibleChange} sx={{ px: 3 }} value={visible ?? ''} variant="scrollable"> <Tabs
onChange={handleVisibleChange}
sx={{ px: 3 }}
value={visible ?? ''}
variant="scrollable"
>
{tabs.map((tab) => ( {tabs.map((tab) => (
<Tab <Tab
icon={<Chip label={tab.count} size="small" variant="soft" />} icon={
<Chip
label={tab.count}
size="small"
variant="soft"
/>
}
iconPosition="end" iconPosition="end"
key={tab.value} key={tab.value}
label={tab.label} label={tab.label}
@@ -209,8 +217,16 @@ export function LessonCategoriesFilters({
))} ))}
</Tabs> </Tabs>
<Divider /> <Divider />
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap', px: 3, py: 2 }}> <Stack
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto', flexWrap: 'wrap' }}> direction="row"
spacing={2}
sx={{ alignItems: 'center', flexWrap: 'wrap', px: 3, py: 2 }}
>
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center', flex: '1 1 auto', flexWrap: 'wrap' }}
>
<FilterButton <FilterButton
displayValue={name} displayValue={name}
label={t('Name')} label={t('Name')}
@@ -240,16 +256,31 @@ export function LessonCategoriesFilters({
{hasFilters ? <Button onClick={handleClearFilters}>{t('Clear filters')}</Button> : null} {hasFilters ? <Button onClick={handleClearFilters}>{t('Clear filters')}</Button> : null}
</Stack> </Stack>
{selection.selectedAny ? ( {selection.selectedAny ? (
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}> <Stack
<Typography color="text.secondary" variant="body2"> direction="row"
spacing={2}
sx={{ alignItems: 'center' }}
>
<Typography
color="text.secondary"
variant="body2"
>
{selection.selected.size} {t('selected')} {selection.selected.size} {t('selected')}
</Typography> </Typography>
<Button color="error" variant="contained"> <Button
color="error"
variant="contained"
>
{t('Delete')} {t('Delete')}
</Button> </Button>
</Stack> </Stack>
) : null} ) : null}
<Select name="sort" onChange={handleSortChange} sx={{ maxWidth: '100%', width: '120px' }} value={sortDir}> <Select
name="sort"
onChange={handleSortChange}
sx={{ maxWidth: '100%', width: '120px' }}
value={sortDir}
>
<Option value="desc">{t('Newest')}</Option> <Option value="desc">{t('Newest')}</Option>
<Option value="asc">{t('Oldest')}</Option> <Option value="asc">{t('Oldest')}</Option>
</Select> </Select>
@@ -268,7 +299,12 @@ function TypeFilterPopover(): React.JSX.Element {
}, [initialValue]); }, [initialValue]);
return ( return (
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title={t('Filter by type')}> <FilterPopover
anchorEl={anchorEl}
onClose={onClose}
open={open}
title={t('Filter by type')}
>
<FormControl> <FormControl>
<OutlinedInput <OutlinedInput
onChange={(event) => { onChange={(event) => {
@@ -304,7 +340,12 @@ function NameFilterPopover(): React.JSX.Element {
}, [initialValue]); }, [initialValue]);
return ( return (
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title={t('Filter by name')}> <FilterPopover
anchorEl={anchorEl}
onClose={onClose}
open={open}
title={t('Filter by name')}
>
<FormControl> <FormControl>
<OutlinedInput <OutlinedInput
onChange={(event) => { onChange={(event) => {
@@ -339,7 +380,12 @@ function EmailFilterPopover(): React.JSX.Element {
}, [initialValue]); }, [initialValue]);
return ( return (
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title="Filter by email"> <FilterPopover
anchorEl={anchorEl}
onClose={onClose}
open={open}
title="Filter by email"
>
<FormControl> <FormControl>
<OutlinedInput <OutlinedInput
onChange={(event) => { onChange={(event) => {
@@ -374,7 +420,12 @@ function PhoneFilterPopover(): React.JSX.Element {
}, [initialValue]); }, [initialValue]);
return ( return (
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title="Filter by phone number"> <FilterPopover
anchorEl={anchorEl}
onClose={onClose}
open={open}
title="Filter by phone number"
>
<FormControl> <FormControl>
<OutlinedInput <OutlinedInput
onChange={(event) => { onChange={(event) => {

View File

@@ -10,7 +10,7 @@ function noop(): void {
return undefined; return undefined;
} }
interface LessonCategoriesPaginationProps { interface VocabulariesPaginationProps {
count: number; count: number;
page: number; page: number;
// //
@@ -19,14 +19,14 @@ interface LessonCategoriesPaginationProps {
rowsPerPage: number; rowsPerPage: number;
} }
export function LessonCategoriesPagination({ export function VocabulariesPagination({
count, count,
page, page,
// //
setPage, setPage,
setRowsPerPage, setRowsPerPage,
rowsPerPage, rowsPerPage,
}: LessonCategoriesPaginationProps): React.JSX.Element { }: VocabulariesPaginationProps): React.JSX.Element {
// You should implement the pagination using a similar logic as the filters. // You should implement the pagination using a similar logic as the filters.
// Note that when page change, you should keep the filter search params. // Note that when page change, you should keep the filter search params.
const handleChangePage = (event: unknown, newPage: number) => { const handleChangePage = (event: unknown, newPage: number) => {

View File

@@ -0,0 +1,46 @@
'use client';
import * as React from 'react';
import { useSelection } from '@/hooks/use-selection';
import type { Selection } from '@/hooks/use-selection';
import { Vocabulary } from './type';
function noop(): void {
return undefined;
}
export interface VocabulariesSelectionContextValue extends Selection {}
export const VocabulariesSelectionContext = React.createContext<VocabulariesSelectionContextValue>({
deselectAll: noop,
deselectOne: noop,
selectAll: noop,
selectOne: noop,
selected: new Set(),
selectedAny: false,
selectedAll: false,
});
interface VocabulariesSelectionProviderProps {
children: React.ReactNode;
lessonCategories: Vocabulary[];
}
export function VocabulariesSelectionProvider({
children,
lessonCategories = [],
}: VocabulariesSelectionProviderProps): React.JSX.Element {
const customerIds = React.useMemo(() => lessonCategories.map((customer) => customer.id), [lessonCategories]);
const selection = useSelection(customerIds);
return (
<VocabulariesSelectionContext.Provider value={{ ...selection }}>{children}</VocabulariesSelectionContext.Provider>
);
}
export function useVocabulariesSelection(): VocabulariesSelectionContextValue {
return React.useContext(VocabulariesSelectionContext);
}

View File

@@ -27,10 +27,18 @@ import { DataTable } from '@/components/core/data-table';
import type { ColumnDef } from '@/components/core/data-table'; import type { ColumnDef } from '@/components/core/data-table';
import ConfirmDeleteModal from './confirm-delete-modal'; import ConfirmDeleteModal from './confirm-delete-modal';
import { useLessonCategoriesSelection } from './lesson-categories-selection-context'; import { useVocabulariesSelection } from './vocabularies-selection-context';
import type { LessonCategory } from './type'; import type { Vocabulary } from './type';
import { listLessonCategories } from '@/db/LessonCategories/listLessonCategories';
import { LessonCategory } from '@/db/LessonCategories/type';
import { Logger } from '@/lib/logger';
import { logger } from '@/lib/default-logger';
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonCategory>[] { function columns(
handleDeleteClick: (testId: string) => void,
lessonCategories: { id: string; label: string }[]
): ColumnDef<Vocabulary>[] {
return [ return [
{ {
formatter: (row): React.JSX.Element => ( formatter: (row): React.JSX.Element => (
@@ -42,7 +50,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonC
<Link <Link
color="inherit" color="inherit"
component={RouterLink} component={RouterLink}
href={paths.dashboard.lesson_categories.details(row.id)} href={paths.dashboard.vocabularies.details(row.id)}
sx={{ whiteSpace: 'nowrap' }} sx={{ whiteSpace: 'nowrap' }}
variant="subtitle2" variant="subtitle2"
> >
@@ -58,12 +66,12 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonC
<ImagesIcon size={32} /> <ImagesIcon size={32} />
</Avatar>{' '} </Avatar>{' '}
<div> <div>
<Box sx={{ whiteSpace: 'nowrap' }}>{row.cat_name}</Box> <Box sx={{ whiteSpace: 'nowrap' }}>{row.word}</Box>
<Typography <Typography
color="text.secondary" color="text.secondary"
variant="body2" variant="body2"
> >
slug: {row.cat_name} slug: {row.word_c}
</Typography> </Typography>
</div> </div>
</Stack> </Stack>
@@ -74,78 +82,39 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonC
width: '200px', width: '200px',
}, },
{ {
formatter: (row): React.JSX.Element => ( formatter: (row): React.JSX.Element => {
const { cat_name: catName } = row?.expand?.cat_id ?? { cat_name: '--' };
return (
<Stack <Stack
direction="row" direction="row"
spacing={2} spacing={2}
sx={{ alignItems: 'center' }} sx={{ alignItems: 'center' }}
> >
<LinearProgress
sx={{ flex: '1 1 auto' }}
value={row.quota}
variant="determinate"
/>
<Typography <Typography
color="text.secondary" color="text.secondary"
variant="body2" variant="body2"
> >
{new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} {catName}
</Typography> </Typography>
</Stack> </Stack>
), );
},
// NOTE: please refer to translation.json here // NOTE: please refer to translation.json here
name: 'word-count', name: 'category',
width: '100px', width: '100px',
}, },
{ {
formatter: (row): React.JSX.Element => { formatter: (row): React.JSX.Element => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const mapping = {
active: {
label: 'Active',
icon: (
<CheckCircleIcon
color="var(--mui-palette-success-main)"
weight="fill"
/>
),
},
blocked: { label: 'Blocked', icon: <MinusIcon color="var(--mui-palette-error-main)" /> },
pending: {
label: 'Pending',
icon: (
<ClockIcon
color="var(--mui-palette-warning-main)"
weight="fill"
/>
),
},
NA: {
label: 'NA',
icon: (
<ClockIcon
color="var(--mui-palette-warning-main)"
weight="fill"
/>
),
},
} as const;
const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null };
return ( return (
<Button <Chip
onClick={() => { label={row.visible}
toast.error('sorry but not implementd'); variant="outlined"
}} size="small"
style={{ backgroundColor: 'transparent' }} />
>
{/* <Chip icon={icon} label={label} size="small" variant="outlined" /> */}
{label}
</Button>
); );
}, },
name: 'Status', name: 'visible',
width: '150px', width: '150px',
}, },
{ {
@@ -165,13 +134,12 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonC
// //
color="secondary" color="secondary"
component={RouterLink} component={RouterLink}
href={paths.dashboard.lesson_categories.details(row.id)} href={paths.dashboard.vocabularies.details(row.id)}
> >
<PencilSimpleIcon size={24} /> <PencilSimpleIcon size={24} />
</LoadingButton> </LoadingButton>
<LoadingButton <LoadingButton
color="error" color="error"
disabled={row.isEmpty}
onClick={() => { onClick={() => {
handleDeleteClick(row.id); handleDeleteClick(row.id);
}} }}
@@ -188,14 +156,14 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonC
]; ];
} }
export interface LessonCategoriesTableProps { export interface VocabulariesTableProps {
rows: LessonCategory[]; rows: Vocabulary[];
reloadRows: () => void; reloadRows: () => void;
} }
export function LessonCategoriesTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element { export function VocabulariesTable({ rows, reloadRows }: VocabulariesTableProps): React.JSX.Element {
const { t } = useTranslation(['lesson_category']); const { t } = useTranslation(['vocabulary']);
const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLessonCategoriesSelection(); const { deselectAll, deselectOne, selectAll, selectOne, selected } = useVocabulariesSelection();
const [idToDelete, setIdToDelete] = React.useState(''); const [idToDelete, setIdToDelete] = React.useState('');
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
@@ -205,6 +173,20 @@ export function LessonCategoriesTable({ rows, reloadRows }: LessonCategoriesTabl
setIdToDelete(testId); setIdToDelete(testId);
} }
let [lessonCategories, setLessonCategories] = React.useState<{}>({});
async function tempFunc(): Promise<void> {
try {
const tempCategories = await listLessonCategories();
console.log(tempCategories);
} catch (error) {
logger.error(error);
}
}
React.useEffect(() => {
void tempFunc;
}, []);
return ( return (
<React.Fragment> <React.Fragment>
<ConfirmDeleteModal <ConfirmDeleteModal
@@ -213,7 +195,7 @@ export function LessonCategoriesTable({ rows, reloadRows }: LessonCategoriesTabl
reloadRows={reloadRows} reloadRows={reloadRows}
setOpen={setOpen} setOpen={setOpen}
/> />
<DataTable<LessonCategory> <DataTable<Vocabulary>
columns={columns(handleDeleteClick)} columns={columns(handleDeleteClick)}
onDeselectAll={deselectAll} onDeselectAll={deselectAll}
onDeselectOne={(_, row) => { onDeselectOne={(_, row) => {

View File

@@ -3,23 +3,19 @@
import * as React from 'react'; import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { COL_LESSON_CATEGORIES, NS_LESSON_CATEGORY } from '@/constants';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import { Avatar, Divider, MenuItem } from '@mui/material'; import { Avatar, Divider } from '@mui/material';
// import Avatar from '@mui/material/Avatar'; // import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Card from '@mui/material/Card'; import Card from '@mui/material/Card';
import CardActions from '@mui/material/CardActions'; import CardActions from '@mui/material/CardActions';
import CardContent from '@mui/material/CardContent'; import CardContent from '@mui/material/CardContent';
import Checkbox from '@mui/material/Checkbox';
import FormControl from '@mui/material/FormControl'; import FormControl from '@mui/material/FormControl';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormHelperText from '@mui/material/FormHelperText'; import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel'; import InputLabel from '@mui/material/InputLabel';
import OutlinedInput from '@mui/material/OutlinedInput'; import OutlinedInput from '@mui/material/OutlinedInput';
import Select from '@mui/material/Select';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Unstable_Grid2'; import Grid from '@mui/material/Unstable_Grid2';
@@ -32,7 +28,6 @@ import { z as zod } from 'zod';
import { paths } from '@/paths'; import { paths } from '@/paths';
import { logger } from '@/lib/default-logger'; import { logger } from '@/lib/default-logger';
import { fileToBase64 } from '@/lib/file-to-base64'; import { fileToBase64 } from '@/lib/file-to-base64';
import { Option } from '@/components/core/option';
import { TextEditor } from '@/components/core/text-editor/text-editor'; import { TextEditor } from '@/components/core/text-editor/text-editor';
import { toast } from '@/components/core/toaster'; import { toast } from '@/components/core/toaster';
@@ -71,12 +66,10 @@ const defaultValues = {
currency: 'USD', currency: 'USD',
} satisfies Values; } satisfies Values;
export function LessonCategoryCreateForm(): React.JSX.Element { export function VocabularyCreateForm(): React.JSX.Element {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(['common', 'lesson_category']); const { t } = useTranslation(['common', 'lesson_category']);
const NS_DEFAULT = { ns: 'lesson_category' };
const [isCreating, setIsCreating] = React.useState<boolean>(false); const [isCreating, setIsCreating] = React.useState<boolean>(false);
const { const {
@@ -121,12 +114,22 @@ export function LessonCategoryCreateForm(): React.JSX.Element {
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<Card> <Card>
<CardContent> <CardContent>
<Stack divider={<Divider />} spacing={4}> <Stack
divider={<Divider />}
spacing={4}
>
<Stack spacing={3}> <Stack spacing={3}>
<Typography variant="h6">{t('create.typeInformation', NS_DEFAULT)}</Typography> <Typography variant="h6">{t('create.typeInformation')}</Typography>
<Grid container spacing={3}> <Grid
container
spacing={3}
>
<Grid xs={12}> <Grid xs={12}>
<Stack direction="row" spacing={3} sx={{ alignItems: 'center' }}> <Stack
direction="row"
spacing={3}
sx={{ alignItems: 'center' }}
>
<Box <Box
sx={{ sx={{
border: '1px dashed var(--mui-palette-divider)', border: '1px dashed var(--mui-palette-divider)',
@@ -151,9 +154,12 @@ export function LessonCategoryCreateForm(): React.JSX.Element {
<CameraIcon fontSize="var(--Icon-fontSize)" /> <CameraIcon fontSize="var(--Icon-fontSize)" />
</Avatar> </Avatar>
</Box> </Box>
<Stack spacing={1} sx={{ alignItems: 'flex-start' }}> <Stack
<Typography variant="subtitle1">{t('create.avatar', NS_DEFAULT)}</Typography> spacing={1}
<Typography variant="caption">{t('create.avatarRequirements', NS_DEFAULT)}</Typography> sx={{ alignItems: 'flex-start' }}
>
<Typography variant="subtitle1">{t('create.avatar')}</Typography>
<Typography variant="caption">{t('create.avatarRequirements')}</Typography>
<Button <Button
color="secondary" color="secondary"
onClick={() => { onClick={() => {
@@ -161,44 +167,70 @@ export function LessonCategoryCreateForm(): React.JSX.Element {
}} }}
variant="outlined" variant="outlined"
> >
{t('create.avatar_select', NS_DEFAULT)} {t('create.avatar_select')}
</Button> </Button>
<input hidden onChange={handleAvatarChange} ref={avatarInputRef} type="file" /> <input
hidden
onChange={handleAvatarChange}
ref={avatarInputRef}
type="file"
/>
</Stack> </Stack>
</Stack> </Stack>
</Grid> </Grid>
<Grid md={6} xs={12}> <Grid
md={6}
xs={12}
>
<Controller <Controller
control={control} control={control}
name="name" name="name"
render={({ field }) => ( render={({ field }) => (
<FormControl error={Boolean(errors.name)} fullWidth> <FormControl
<InputLabel required>{t('create.name', NS_DEFAULT)}</InputLabel> error={Boolean(errors.name)}
fullWidth
>
<InputLabel required>{t('create.name')}</InputLabel>
<OutlinedInput {...field} /> <OutlinedInput {...field} />
{errors.name ? <FormHelperText>{errors.name.message}</FormHelperText> : null} {errors.name ? <FormHelperText>{errors.name.message}</FormHelperText> : null}
</FormControl> </FormControl>
)} )}
/> />
</Grid> </Grid>
<Grid md={6} xs={12}> <Grid
md={6}
xs={12}
>
<Controller <Controller
control={control} control={control}
name="email" name="email"
render={({ field }) => ( render={({ field }) => (
<FormControl error={Boolean(errors.email)} fullWidth> <FormControl
error={Boolean(errors.email)}
fullWidth
>
<InputLabel required>Email address</InputLabel> <InputLabel required>Email address</InputLabel>
<OutlinedInput {...field} type="email" /> <OutlinedInput
{...field}
type="email"
/>
{errors.email ? <FormHelperText>{errors.email.message}</FormHelperText> : null} {errors.email ? <FormHelperText>{errors.email.message}</FormHelperText> : null}
</FormControl> </FormControl>
)} )}
/> />
</Grid> </Grid>
<Grid md={6} xs={12}> <Grid
md={6}
xs={12}
>
<Controller <Controller
control={control} control={control}
name="phone" name="phone"
render={({ field }) => ( render={({ field }) => (
<FormControl error={Boolean(errors.phone)} fullWidth> <FormControl
error={Boolean(errors.phone)}
fullWidth
>
<InputLabel required>Phone number</InputLabel> <InputLabel required>Phone number</InputLabel>
<OutlinedInput {...field} /> <OutlinedInput {...field} />
{errors.phone ? <FormHelperText>{errors.phone.message}</FormHelperText> : null} {errors.phone ? <FormHelperText>{errors.phone.message}</FormHelperText> : null}
@@ -206,12 +238,18 @@ export function LessonCategoryCreateForm(): React.JSX.Element {
)} )}
/> />
</Grid> </Grid>
<Grid md={6} xs={12}> <Grid
md={6}
xs={12}
>
<Controller <Controller
control={control} control={control}
name="company" name="company"
render={({ field }) => ( render={({ field }) => (
<FormControl error={Boolean(errors.company)} fullWidth> <FormControl
error={Boolean(errors.company)}
fullWidth
>
<InputLabel>Company</InputLabel> <InputLabel>Company</InputLabel>
<OutlinedInput {...field} /> <OutlinedInput {...field} />
{errors.company ? <FormHelperText>{errors.company.message}</FormHelperText> : null} {errors.company ? <FormHelperText>{errors.company.message}</FormHelperText> : null}
@@ -222,35 +260,56 @@ export function LessonCategoryCreateForm(): React.JSX.Element {
</Grid> </Grid>
</Stack> </Stack>
<Stack spacing={3}> <Stack spacing={3}>
<Typography variant="h6">{t('create.detail-information', NS_DEFAULT)}</Typography> <Typography variant="h6">{t('create.detail-information')}</Typography>
<Grid container spacing={3}> <Grid
<Grid md={6} xs={12}> container
spacing={3}
>
<Grid
md={6}
xs={12}
>
<Controller <Controller
control={control} control={control}
name="billingAddress.country" name="billingAddress.country"
render={({ field }) => ( render={({ field }) => (
<Box> <Box>
<Typography variant="subtitle1" color="text-secondary"> <Typography
{t('create.description', NS_DEFAULT)} variant="subtitle1"
color="text-secondary"
>
{t('create.description')}
</Typography> </Typography>
<Box sx={{ mt: '8px', '& .tiptap-container': { height: '400px' } }}> <Box sx={{ mt: '8px', '& .tiptap-container': { height: '400px' } }}>
<TextEditor content="" placeholder="Write something" /> <TextEditor
content=""
placeholder="Write something"
/>
</Box> </Box>
</Box> </Box>
)} )}
/> />
</Grid> </Grid>
<Grid md={6} xs={12}> <Grid
md={6}
xs={12}
>
<Controller <Controller
control={control} control={control}
name="billingAddress.state" name="billingAddress.state"
render={({ field }) => ( render={({ field }) => (
<Box> <Box>
<Typography variant="subtitle1" color="text.secondary"> <Typography
{t('create.remarks', NS_DEFAULT)} variant="subtitle1"
color="text.secondary"
>
{t('create.remarks')}
</Typography> </Typography>
<Box sx={{ mt: '8px', '& .tiptap-container': { height: '400px' } }}> <Box sx={{ mt: '8px', '& .tiptap-container': { height: '400px' } }}>
<TextEditor content="" placeholder="Write something" /> <TextEditor
content=""
placeholder="Write something"
/>
</Box> </Box>
</Box> </Box>
)} )}
@@ -261,12 +320,21 @@ export function LessonCategoryCreateForm(): React.JSX.Element {
</Stack> </Stack>
</CardContent> </CardContent>
<CardActions sx={{ justifyContent: 'flex-end' }}> <CardActions sx={{ justifyContent: 'flex-end' }}>
<Button color="secondary" component={RouterLink} href={paths.dashboard.lesson_categories.list}> <Button
{t('create.cancelButton', NS_DEFAULT)} color="secondary"
component={RouterLink}
href={paths.dashboard.lesson_categories.list}
>
{t('create.cancelButton')}
</Button> </Button>
<LoadingButton disabled={isCreating} loading={isCreating} type="submit" variant="contained"> <LoadingButton
{t('create.createButton', NS_DEFAULT)} disabled={isCreating}
loading={isCreating}
type="submit"
variant="contained"
>
{t('create.createButton')}
</LoadingButton> </LoadingButton>
</CardActions> </CardActions>
</Card> </Card>

View File

@@ -0,0 +1,462 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import { COL_VOCABULARIES } from '@/constants';
import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab';
import { Avatar, Divider, MenuItem } from '@mui/material';
// import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardActions from '@mui/material/CardActions';
import CardContent from '@mui/material/CardContent';
import FormControl from '@mui/material/FormControl';
import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel';
import OutlinedInput from '@mui/material/OutlinedInput';
import Select from '@mui/material/Select';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Unstable_Grid2';
import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera';
import type { RecordModel } from 'pocketbase';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import { dayjs } from '@/lib/dayjs';
import { logger } from '@/lib/default-logger';
import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
import { pb } from '@/lib/pb';
import { Option } from '@/components/core/option';
import { TextEditor } from '@/components/core/text-editor/text-editor';
import { toast } from '@/components/core/toaster';
import FormLoading from '@/components/loading';
import ErrorDisplay from '../error';
import { defaultLessonCategory } from './_constants';
import type { EditFormProps } from './type';
import getVocabularyById from '@/db/Vocabularies/GetById';
import getAllLessonCategories from '@/db/LessonCategories/GetAll';
import { listLessonCategories } from '@/db/LessonCategories/listLessonCategories';
import isDevelopment from '@/lib/check-is-development';
const schema = zod.object({
image: zod.union([zod.array(zod.any()), zod.string()]).optional(),
sound: zod.union([zod.array(zod.any()), zod.string()]).optional(),
word: zod.string().min(1, 'Word is required').max(255),
word_c: zod.string().min(1, 'Chinese word is required').max(255),
sample_e: zod.string().optional(),
sample_c: zod.string().optional(),
cat_id: zod.string().min(1, 'Category ID is required'),
category: zod.string().optional(),
lesson_type_id: zod.string().min(1, 'Lesson type ID is required'),
// NOTE: for image handling
avatar: zod.string().optional(),
});
type Values = zod.infer<typeof schema>;
const defaultValues = {
image: undefined,
sound: undefined,
word: '',
word_c: '',
sample_e: '',
sample_c: '',
cat_id: '',
category: '',
lesson_type_id: '',
} satisfies Values;
export function VocabularyEditForm(): React.JSX.Element {
const router = useRouter();
const { t } = useTranslation();
const { cat_id: catId } = useParams<{ cat_id: string }>();
//
const [isUpdating, setIsUpdating] = React.useState<boolean>(false);
const [showLoading, setShowLoading] = React.useState<boolean>(false);
//
const [showError, setShowError] = React.useState({ show: false, detail: '' });
const {
control,
handleSubmit,
formState: { errors },
setValue,
reset,
watch,
} = useForm<Values>({ defaultValues, resolver: zodResolver(schema) });
const onSubmit = React.useCallback(
async (values: Values): Promise<void> => {
setIsUpdating(true);
const tempUpdate: EditFormProps = {
image: values.avatar ? [await base64ToFile(values.avatar)] : null,
sound: '',
word: values.word,
word_c: values.word_c,
sample_e: values.sample_e || '',
sample_c: values.sample_c || '',
cat_id: values.cat_id,
category: '',
lesson_type_id: '',
};
//
try {
console.log({ tempUpdate });
const result = await pb.collection(COL_VOCABULARIES).update(catId, tempUpdate);
logger.debug(result);
toast.success(t('edit.success'));
router.push(paths.dashboard.vocabularies.list);
} catch (error) {
logger.error(error);
toast.error(t('update.failed'));
} finally {
setIsUpdating(false);
}
},
// t is not necessary here
// eslint-disable-next-line react-hooks/exhaustive-deps
[router]
);
const avatarInputRef = React.useRef<HTMLInputElement>(null);
const avatar = watch('avatar');
const handleAvatarChange = React.useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const url = await fileToBase64(file);
setValue('avatar', url);
}
},
[setValue]
);
// load existing data when user arrive
const loadExistingData = React.useCallback(
async (id: string) => {
try {
const result = await pb.collection(COL_VOCABULARIES).getOne(id);
reset({ ...defaultValues, ...result });
if (result.image !== '') {
const fetchResult = await fetch(
`http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.image}`
);
const blob = await fetchResult.blob();
const url = await fileToBase64(blob);
setValue('avatar', url);
} else {
setValue('avatar', '');
}
} catch (error) {
logger.error(error);
toast(t('list.error'));
setShowError({ show: true, detail: JSON.stringify(error, null, 2) });
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[catId]
);
let [categoriesOption, setCategoriesOption] = React.useState<{ value: string; label: string }[]>([]);
const loadCategories = React.useCallback(async () => {
try {
const categories = await listLessonCategories();
logger.debug(categories);
setCategoriesOption(
categories.map((c) => {
return { label: c.cat_name || '??', value: c.id };
})
);
} catch (error) {
logger.error(error);
toast.error(t('list.error'));
}
}, [catId]);
React.useEffect(() => {
setShowLoading(true);
void loadCategories();
void loadExistingData(catId);
setShowLoading(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [catId]);
if (showLoading) return <FormLoading />;
if (showError.show)
return (
<ErrorDisplay
message={t('error.unable-to-process-request')}
code="500"
details={showError.detail}
/>
);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Card>
<CardContent>
<Stack
divider={<Divider />}
spacing={4}
>
<Stack spacing={3}>
<Typography variant="h6">{t('edit.basic-info')}</Typography>
<Grid
container
spacing={3}
>
<Grid xs={12}>
<Stack
direction="row"
spacing={3}
sx={{ alignItems: 'center' }}
>
<Box
sx={{
border: '1px dashed var(--mui-palette-divider)',
borderRadius: '5%',
display: 'inline-flex',
p: '4px',
}}
>
<Avatar
variant="rounded"
src={avatar}
sx={{
'--Avatar-size': '100px',
'--Icon-fontSize': 'var(--icon-fontSize-lg)',
alignItems: 'center',
bgcolor: 'var(--mui-palette-background-level1)',
color: 'var(--mui-palette-text-primary)',
display: 'flex',
justifyContent: 'center',
}}
>
<CameraIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
</Box>
<Stack
spacing={1}
sx={{ alignItems: 'flex-start' }}
>
<Typography variant="subtitle1">{t('edit.avatar')}</Typography>
<Typography variant="caption">{t('edit.avatarRequirements')}</Typography>
<Button
color="secondary"
onClick={() => {
avatarInputRef.current?.click();
}}
variant="outlined"
>
{t('edit.avatar_select')}
</Button>
<input
hidden
onChange={handleAvatarChange}
ref={avatarInputRef}
type="file"
/>
</Stack>
</Stack>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
disabled={isUpdating}
control={control}
name="word"
render={({ field }) => (
<FormControl
disabled={isUpdating}
error={Boolean(errors.word)}
fullWidth
>
<InputLabel required>{t('edit.word')}</InputLabel>
<OutlinedInput {...field} />
{errors.word ? <FormHelperText>{errors.word.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
{/* */}
<Grid
md={6}
xs={12}
>
<Controller
disabled={isUpdating}
control={control}
name="word_c"
render={({ field }) => (
<FormControl
error={Boolean(errors.word_c)}
fullWidth
>
<InputLabel required>{t('edit.word_c')}</InputLabel>
<OutlinedInput {...field} />
{errors.word_c ? <FormHelperText>{errors.word_c.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
{/* */}
<Grid
md={6}
xs={12}
>
<Controller
disabled={isUpdating}
control={control}
name="cat_id"
render={({ field }) => (
<FormControl
error={Boolean(errors.cat_id)}
fullWidth
>
<InputLabel>{t('edit.cat_id')}</InputLabel>
<Select {...field}>
{categoriesOption.map((co, i) => (
<Option
key={i}
value={co.value}
>
{co.label}
</Option>
))}
</Select>
</FormControl>
)}
/>
</Grid>
{/* */}
<Grid
md={6}
xs={12}
>
<Controller
disabled={isUpdating}
control={control}
name="sound"
render={({ field }) => (
<FormControl
error={Boolean(errors.sound)}
fullWidth
>
{/* TODO: sound file selection is not implemented */}
<InputLabel required>{t('edit.sound_file')} - (Not implemented)</InputLabel>
<OutlinedInput {...field} />
{errors.sound ? <FormHelperText>{errors.sound.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
</Grid>
</Stack>
{/* */}
<Stack spacing={3}>
<Typography variant="h6">{t('edit.sample-sentence')}</Typography>
<Grid
container
spacing={3}
>
<Grid
md={6}
xs={12}
>
<Controller
disabled={isUpdating}
control={control}
name="sample_e"
render={({ field }) => (
<FormControl
error={Boolean(errors.sample_e)}
fullWidth
>
<InputLabel>{t('edit.sample_e')}</InputLabel>
<OutlinedInput
{...field}
multiline
rows={4}
/>
{errors.sample_e ? <FormHelperText>{errors.sample_e.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
disabled={isUpdating}
control={control}
name="sample_c"
render={({ field }) => (
<FormControl
error={Boolean(errors.sample_c)}
fullWidth
>
<InputLabel>{t('edit.sample_c')}</InputLabel>
<OutlinedInput
{...field}
multiline
rows={4}
/>
{errors.sample_c ? <FormHelperText>{errors.sample_c.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
</Grid>
</Stack>
{/* */}
</Stack>
</CardContent>
<CardActions sx={{ justifyContent: 'flex-end' }}>
<Button
color="secondary"
component={RouterLink}
href={paths.dashboard.vocabularies.list}
>
{t('edit.cancelButton')}
</Button>
<LoadingButton
disabled={isUpdating}
loading={isUpdating}
type="submit"
variant="contained"
>
1{t('edit.updateButton')}2
</LoadingButton>
</CardActions>
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
<pre>{JSON.stringify(errors, null, 2)}</pre>
</Box>
</Card>
</form>
);
}

View File

@@ -23,11 +23,13 @@ const COL_CUSTOMERS = 'Customers';
const COL_NOTIFICATIONS = 'Notifications'; const COL_NOTIFICATIONS = 'Notifications';
const COL_TEACHERS = 'Teachers'; const COL_TEACHERS = 'Teachers';
const COL_STUDENTS = 'Students'; const COL_STUDENTS = 'Students';
const COL_VOCABULARIES = 'Vocabularies';
// FOR page display // FOR page display
const NO_VALUE = 'NO_VALUE'; const NO_VALUE = 'NO_VALUE';
const NO_NUM = -Infinity; const NO_NUM = -Infinity;
const NS_LESSON_CATEGORY = 'lesson_category'; const NS_LESSON_CATEGORY = 'lesson_category';
const NS_VOCABULARY = 'vocabulary';
export { export {
COL_LESSON_TYPES, COL_LESSON_TYPES,
@@ -51,5 +53,7 @@ export {
COL_NOTIFICATIONS, COL_NOTIFICATIONS,
COL_TEACHERS, COL_TEACHERS,
COL_STUDENTS, COL_STUDENTS,
COL_VOCABULARIES,
NS_VOCABULARY,
// //
}; };

View File

@@ -0,0 +1,13 @@
//
// RULES:
import { COL_LESSON_CATEGORIES } from '@/constants';
import { pb } from '@/lib/pb';
import type { LessonCategory } from './type';
export async function listLessonCategories(): Promise<LessonCategory[]> {
const records = await pb.collection(COL_LESSON_CATEGORIES).getFullList({
expand: 'cat_id',
});
return records as unknown as LessonCategory[];
}

View File

@@ -0,0 +1,23 @@
export interface LessonCategory {
isEmpty?: boolean;
//
id: string;
collectionId: string;
//
cat_name: string;
cat_image_url?: string;
cat_image?: string;
pos: number;
visible: string;
lesson_id: string;
description: string;
remarks: string;
createdAt: Date;
//
name: string;
avatar: string;
email: string;
phone: string;
quota: number;
status: 'pending' | 'active' | 'blocked' | 'NA';
}

View File

@@ -1,3 +1,5 @@
//
// RULES:
// api method for get notifications by user id // api method for get notifications by user id
import { pb } from '@/lib/pb'; import { pb } from '@/lib/pb';
import { COL_NOTIFICATIONS } from '@/constants'; import { COL_NOTIFICATIONS } from '@/constants';

View File

@@ -0,0 +1,8 @@
import { COL_VOCABULARIES } from '@/constants';
import type { Vocabulary, VocabularyCreate } from './type';
import { pb } from '@/lib/pb';
export default function createVocabulary(data: VocabularyCreate): Promise<Vocabulary> {
return pb.collection(COL_VOCABULARIES).create(data);
}

View File

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

View File

@@ -0,0 +1,8 @@
import { COL_VOCABULARIES } from '@/constants';
import type { Vocabularies } from './type';
import { pb } from '@/lib/pb';
export default function getAllVocabularies(): Promise<Vocabularies> {
return pb.collection(COL_VOCABULARIES).getFullList();
}

View File

@@ -0,0 +1,14 @@
// RULES:
// error handled by caller
// contain definition to collection only
import { COL_VOCABULARIES } from '@/constants';
import { pb } from '@/lib/pb';
export default function getAllVocabulariesCount(): Promise<number> {
return pb
.collection(COL_VOCABULARIES)
.getList(1, 9999)
.then((res) => res.totalItems);
}

View File

@@ -0,0 +1,8 @@
import { COL_VOCABULARIES } from '@/constants';
import type { Vocabulary } from './type';
import { pb } from '@/lib/pb';
export default function getVocabularyById(id: string): Promise<Vocabulary> {
return pb.collection(COL_VOCABULARIES).getOne(id);
}

View File

@@ -0,0 +1,9 @@
import { COL_VOCABULARIES } from '@/constants';
import { pb } from '@/lib/pb';
export default function getHiddenVocabulariesCount(): Promise<number> {
return pb
.collection(COL_VOCABULARIES)
.getList(1, 9999, { filter: 'status = "hidden"' })
.then((res) => res.totalItems);
}

View File

@@ -0,0 +1,9 @@
import { COL_VOCABULARIES } from '@/constants';
import { pb } from '@/lib/pb';
export default function getVisibleVocabulariesCount(): Promise<number> {
return pb
.collection(COL_VOCABULARIES)
.getList(1, 9999, { filter: 'status = "visible"' })
.then((res) => res.totalItems);
}

View File

@@ -0,0 +1,5 @@
function helloWorld(): string {
return 'helloworld';
}
export { helloWorld };

View File

@@ -0,0 +1,8 @@
import { COL_VOCABULARIES } from '@/constants';
import type { RecordModel } from 'pocketbase';
import { pb } from '@/lib/pb';
import type { EditFormProps } from './type';
export default function updateVocabulary(id: string, data: EditFormProps): Promise<RecordModel> {
return pb.collection(COL_VOCABULARIES).update(id, data);
}

View File

@@ -0,0 +1,30 @@
# GUIDELINES
This folder contains drivers for `LessonCategory`/`LessonCategories` records using PocketBase:
- create (Create.tsx)
- read (GetById.tsx)
- write (Update.tsx)
- count (GetAllCount.tsx)
- delete (Delete.tsx)
- list (GetAll.tsx)
the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src`
## Assumption and Requirements
- assume `pb` is located in `@/lib/pb`
- no need to handle error in this function, i'll handle it in the caller
- type information defined in `@/db/LessonCategories/type.d.tsx`
simple template:
```typescript
import { pb } from '@/lib/pb';
import { COL_LESSON_CATEGORIES } from '@/constants';
export async function createLessonCategory(data: CreateFormProps) {
// ...content
// use direct return of pb.collection (e.g. return pb.collection(xxx))
}
```

View File

@@ -0,0 +1,53 @@
// RULES: interface for handling vocabulary record
export interface Vocabulary {
//
id: string;
collectionId: string;
//
word: string;
word_c: string;
sample_e: string;
sample_c: string;
cat_id: string;
category: string;
lesson_type_id: string;
image: File[];
sound: File[];
}
// RULES: interface for handling create form
export interface CreateForm {
word: string;
word_c: string;
sample_e: string;
sample_c: string;
cat_id: string;
category: string;
lesson_type_id: string;
image: File[];
sound: File[];
isActive: boolean;
order: number;
}
// RULES: interface for handling edit form
export interface EditFormProps {
id: string;
collectionId: string;
word: string;
word_c: string;
sample_e: string;
sample_c: string;
cat_id: string;
category: string;
lesson_type_id: string;
image: File[];
sound: File[];
isActive: boolean;
order: number;
}
// RULES: helloworld self test
export interface Helloworld {
helloworld: string;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
export default function getImageUrlFromFile(collectionId: string, id: string, catImage: string): string {
return `http://127.0.0.1:8090/api/files/${collectionId}/${id}/${catImage}`;
}

View File

@@ -20,13 +20,7 @@
"plugins": [{ "name": "next" }], "plugins": [{ "name": "next" }],
"paths": { "@/*": ["./src/*"] } "paths": { "@/*": ["./src/*"] }
}, },
"include": [ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
//
],
"exclude": [ "exclude": [
"node_modules", "node_modules",
".next", ".next",