Compare commits

...

45 Commits

Author SHA1 Message Date
louiscklaw
f44539bf63 ``refactor Update .gitignore to exclude files with 'old' suffix in any directory`` 2025-05-15 11:35:47 +08:00
louiscklaw
407f622f24 ``refactor Prettier config to add comment placeholder for potential future plugin additions `` 2025-05-15 11:35:40 +08:00
louiscklaw
7e2844dd74 ```
refactor Student and Teacher create/edit forms to implement i18n support, update UI components, and standardize API calls
```
2025-05-15 11:35:29 +08:00
louiscklaw
097918340c ```
refactor Teachers list page to remove outdated implementation, simplifying component structure and improving maintainability
```
2025-05-15 11:31:03 +08:00
louiscklaw
3620837a6a ``refactor UserMeta creation and authentication client to improve documentation and type consistency`` 2025-05-15 11:27:02 +08:00
louiscklaw
c83e8c1b6e ```
refactor Student and UserMeta type definitions to deprecate obsolete fields, standardize structure, and update billing address format
```
2025-05-15 11:13:15 +08:00
louiscklaw
af15a6bce0 ```
refactor Student type definitions to deprecate obsolete fields, standardize structure, and update billing address format
```
2025-05-15 11:12:56 +08:00
louiscklaw
d0215cf23f ```
refactor GetById APIs for Students, Teachers, and UserMetas to use consistent type definitions and expand parameters
```
2025-05-15 11:12:29 +08:00
louiscklaw
e523f80123 ```
refactor UserMeta type definitions to deprecate obsolete fields and standardize structure
```
2025-05-15 11:10:40 +08:00
louiscklaw
7f8f8824a7 ```
refactor getImageUrlFromFile to use Pocketbase records and correct path in teachers routes
```
2025-05-15 11:10:12 +08:00
louiscklaw
2aa96eec62 ```
refactor student and teacher APIs to use COL_USER_METAS collection, standardize role assignment, and update type definitions
```
2025-05-15 09:27:38 +08:00
louiscklaw
ecab41abbf `` fix GetUserById API to include requestKey option for Pocketbase compatibility `` 2025-05-15 09:27:20 +08:00
louiscklaw
5a1832ca89 ```
remove obsolete GetAllCount API and related guidelines from Users.old module
```
2025-05-15 09:27:08 +08:00
louiscklaw
160c93b83d `` refactor GetUserById function and add Create/UpdateUser APIs with type definitions `` 2025-05-15 09:26:36 +08:00
louiscklaw
d4fcc1dd8f ```
add UserActivationEditForm component for user activation management
```
2025-05-15 09:24:41 +08:00
louiscklaw
8e3d463f78 ```
use dynamic route parameter for user ID in UserActivationEditForm
```
2025-05-15 09:24:27 +08:00
louiscklaw
fbf79b040f `` remove placeholder _PROMPT.md file and reference to external draft for UserMeta editing page `` 2025-05-15 09:24:21 +08:00
louiscklaw
e34782844e ```
add user profile navigation logic and update button in SettingContainer
```
2025-05-15 09:19:59 +08:00
louiscklaw
ba8e9cca69 fix translation, 2025-05-15 08:35:33 +08:00
louiscklaw
cc9fe057c1 ```
add abbreviations section and clarify guideline reading requirement
```
2025-05-14 18:45:23 +08:00
louiscklaw
cdd95faa89 ```
add new student menu route and component, update login default values and redirect logic
```
2025-05-14 18:31:04 +08:00
louiscklaw
030fc1a808 update login requirement for mobile, 2025-05-14 18:13:15 +08:00
louiscklaw
05c69481b5 update check session working, 2025-05-14 18:10:22 +08:00
louiscklaw
0fcc194860 in the middle, working for login and logout test, 2025-05-14 17:19:48 +08:00
louiscklaw
56f0f30ffb ```
replace inline loading text with LoadingScreen component in multiple pages
```
2025-05-14 16:27:30 +08:00
louiscklaw
0aefbfaeae fix typo, 2025-05-14 16:25:57 +08:00
louiscklaw
628c72190b fix typo, 2025-05-14 16:18:39 +08:00
louiscklaw
886a314df7 update settings pages in the middle, 2025-05-14 16:18:04 +08:00
louiscklaw
efc2d31f7c ```
add new student info route and related components, update auth guard implementation and signup success redirect
```
2025-05-14 15:40:59 +08:00
louiscklaw
1938e95948 ```
use async/await for authClient.signInWithPassword to ensure proper execution order
```
2025-05-14 15:20:02 +08:00
louiscklaw
8d37fba393 update login flow, in the middle, 2025-05-14 15:17:04 +08:00
louiscklaw
af160edd42 ```
update .gitignore to add api_ts and dist directories to exclusion list
```
2025-05-14 15:16:21 +08:00
louiscklaw
d880420a28 ``add configuration files for CrossNote, VSCode extensions, and documentation for system architecture and design requirements`` 2025-05-14 15:15:45 +08:00
louiscklaw
5bebc1f40e ```
add COL_BILLING_ADDRESS constant and update exports
```
2025-05-13 13:28:21 +08:00
louiscklaw
f4e5f94e17 ``update .gitignore to modify exclusion pattern for _del files from '**/_del' to '**/*del'`` 2025-05-13 13:28:13 +08:00
louiscklaw
2d022cb613 ```
remove Docker Compose configuration files for CMS, Doc, Ionic Mobile, API_TS, and PocketBase services
```
2025-05-13 13:27:56 +08:00
louiscklaw
3560ea79fc "``update teacher and student seed scripts to dynamically loop through row_array and um_row_array instead of fixed iterations``" 2025-05-13 13:27:48 +08:00
louiscklaw
a441e3e52d ```
refactor teacher and user meta management UI by updating form components, replacing COL_TEACHERS with COL_USER_METAS where applicable, adding development environment checks, improving type definitions for user meta including billing address, and fixing parameter naming inconsistencies in form handlers
```
2025-05-13 13:27:41 +08:00
louiscklaw
09ded06dd2 ``update student database operations to use COL_USER_METAS instead of COL_STUDENTS, refactor getStudentById to include expanded billing address data, and add/update related types and functions for student and user meta management`` 2025-05-13 13:27:27 +08:00
louiscklaw
7ecacd0692 ```
refactor student management UI by updating create form component name and adding new edit page with translation support and form integration
```
2025-05-13 13:27:17 +08:00
louiscklaw
8a094afdd2 "``refactor student create and edit forms with translation support, update schema validation, and use new database operations for student management``" 2025-05-13 13:26:58 +08:00
louiscklaw
64ca29cf60 ``add new database operations for billing address module including create, delete, get, update functions and related type definitions`` 2025-05-13 13:26:41 +08:00
louiscklaw
1aa0502edc ``fix inconsistent quotes in code and update schema with additional fields and rules`` 2025-05-13 13:26:22 +08:00
louiscklaw
3e1f2e1057 ```
add billing address seed data and update user seed scripts with teacher and student roles
```
2025-05-13 12:35:05 +08:00
louiscklaw
9be33f641f ```
add 'visible', 'state', 'company', 'taxId', 'timezone', 'language', 'currency' fields and update 'billingAddress' and 't1' collection rules in PocketBase seed schema
```
2025-05-13 12:34:51 +08:00
127 changed files with 3345 additions and 1025 deletions

15
.crossnote/config.js Normal file
View File

@@ -0,0 +1,15 @@
({
katexConfig: {
"macros": {}
},
mathjaxConfig: {
"tex": {},
"options": {},
"loader": {}
},
mermaidConfig: {
"startOnLoad": false
},
})

19
.crossnote/head.html Normal file
View File

@@ -0,0 +1,19 @@
<!-- The content below will be included at the end of the <head> element. -->
<script type="text/javascript">
const configureMermaidIconPacks = () => {
window['mermaid'].registerIconPacks([
{
name: 'logos',
loader: () => fetch('https://unpkg.com/@iconify-json/logos/icons.json').then((res) => res.json()),
},
]);
};
if (document.readyState !== 'loading') {
configureMermaidIconPacks();
} else {
document.addEventListener('DOMContentLoaded', () => {
configureMermaidIconPacks();
});
}
</script>

12
.crossnote/parser.js Normal file
View File

@@ -0,0 +1,12 @@
({
// Please visit the URL below for more information:
// https://shd101wyy.github.io/markdown-preview-enhanced/#/extend-parser
onWillParseMarkdown: async function(markdown) {
return markdown;
},
onDidParseMarkdown: async function(html) {
return html;
},
})

8
.crossnote/style.less Normal file
View File

@@ -0,0 +1,8 @@
/* Please visit the URL below for more information: */
/* https://shd101wyy.github.io/markdown-preview-enhanced/#/customize-css */
.markdown-preview.markdown-preview {
// modify your style here
// eg: background-color: blue;
}

6
.gitignore vendored
View File

@@ -1,3 +1,6 @@
# api_ts
dist
.next
node_modules
005_references/
@@ -7,7 +10,8 @@ _del
*.bak
*.log
*.del
**/_del
**/*del
**/*old
**/volumes/**
006_lab

View File

@@ -1,12 +1,15 @@
{
"recommendations": [
"aflalo.dbml-formatter",
"bierner.markdown-mermaid",
"christian-kohler.path-intellisense",
"esbenp.prettier-vscode",
"humao.rest-client",
//
"matt-meyers.vscode-dbml",
"aflalo.dbml-formatter",
"nicolas-liger.dbml-viewer",
"yzhang.markdown-all-in-one"
//
"bierner.markdown-mermaid",
"yzhang.markdown-all-in-one",
"shd101wyy.markdown-preview-enhanced"
]
}

View File

@@ -15,7 +15,9 @@
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"workbench.iconTheme": "material-icon-theme",
"workbench.colorTheme": "Default Dark Modern",
"workbench.colorTheme": "Mermaid Dark",
"editor.formatOnSave": true,
"git.ignoreLimitWarning": true
"git.ignoreLimitWarning": true,
//
"markdown.styles": ["https://use.fontawesome.com/releases/v5.7.1/css/all.css"]
}

View File

@@ -28,5 +28,10 @@
- `006_lab` my test (POC) of this project
- `README.md` Readme of this project
- `TODO.md` todo list of this project
- `001_documentation/Requirements/REQ0019/index.md` describes updated system architecture
- if the directory contains `_GUIDELINES.md`, please read it before operation
## Abbreviations
T.B.A.

View File

@@ -1,11 +1,11 @@
import { faker } from "@faker-js/faker";
import { faker } from '@faker-js/faker';
const getId = (id) => id.padStart(15, "0");
const getId = (id) => id.padStart(15, '0');
const row_array = Array.from({ length: 10 }, (_, i) => [
getId(String(i + 1)),
faker.person.firstName(),
"",
'',
faker.internet.email(),
faker.phone.number(),
faker.company.name(),
@@ -19,13 +19,11 @@ const row_array = Array.from({ length: 10 }, (_, i) => [
},
Math.floor(Math.random() * (100 - 0 + 1)) + 0,
faker.location.timeZone(),
["en", "de", "es", "fr", "ja", "ko", "zh-CN"].sort(
() => Math.random() - 0.5
)[0],
['en', 'de', 'es', 'fr', 'ja', 'ko', 'zh-CN'].sort(() => Math.random() - 0.5)[0],
faker.finance.currencyCode(),
]);
import fs from "fs";
const filePath = "output.json";
import fs from 'fs';
const filePath = 'output.json';
fs.writeFileSync(filePath, JSON.stringify(row_array, null, 2));
console.log(`Wrote ${row_array.length} records to ${filePath}`);

View File

@@ -1,4 +1,4 @@
// Generated at: 2025-05-12T06:02:53.613Z
// Generated at: 2025-05-13T05:24:33.962Z
//
@@ -206,9 +206,9 @@ Table QuizLPCategories {
cat_image file
pos integer
init_answer text
visible text
created datetime
updated datetime
visible text
slug text
remarks text
description text
@@ -223,8 +223,6 @@ Table QuizLPQuestions {
word text
sound file
cat_id integer [ref: > QuizLPCategories.id] // relation3870140739
created datetime
updated datetime
cat_name text
cat_image file
pos integer
@@ -233,6 +231,8 @@ Table QuizLPQuestions {
slug text
remarks text
description text
created datetime
updated datetime
}
//
@@ -258,9 +258,9 @@ Table QuizMFCategories {
cat_image file
pos integer
init_answer text
visible text
created datetime
updated datetime
visible text
}
//
@@ -341,17 +341,23 @@ Table Teachers {
// collection type: base
Table UserMetas {
id text [pk, not null]
helloworld text
address text
meta text
user_id integer [ref: > users.id] // relation2809058197
state text
created datetime
updated datetime
status text
avatar file
role text
name text
email text
phone text
company text
taxId text
timezone text
language text
currency text
billingAddress integer [ref: > billingAddress.id] // relation2115670734
}
//

View File

@@ -1750,6 +1750,20 @@
"system": false,
"type": "json"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2058414169",
"max": 0,
"min": 0,
"name": "visible",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "autodate2990389176",
@@ -1770,20 +1784,6 @@
"system": false,
"type": "autodate"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2058414169",
"max": 0,
"min": 0,
"name": "visible",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
@@ -1892,26 +1892,6 @@
"system": false,
"type": "relation"
},
{
"hidden": false,
"id": "autodate2990389176",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"autogeneratePattern": "",
"hidden": false,
@@ -2014,6 +1994,26 @@
"required": false,
"system": false,
"type": "editor"
},
{
"hidden": false,
"id": "autodate2990389176",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"indexes": [],
@@ -2182,6 +2182,20 @@
"system": false,
"type": "json"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2058414169",
"max": 0,
"min": 0,
"name": "visible",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "autodate2990389176",
@@ -2201,20 +2215,6 @@
"presentable": false,
"system": false,
"type": "autodate"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2058414169",
"max": 0,
"min": 0,
"name": "visible",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"indexes": [],
@@ -2870,7 +2870,7 @@
"id": "text4192936109",
"max": 0,
"min": 0,
"name": "helloworld",
"name": "address",
"pattern": "",
"presentable": false,
"primaryKey": false,
@@ -2901,6 +2901,20 @@
"system": false,
"type": "relation"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2744374011",
"max": 0,
"min": 0,
"name": "state",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "autodate2990389176",
@@ -2921,20 +2935,6 @@
"system": false,
"type": "autodate"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2744374011",
"max": 0,
"min": 0,
"name": "status",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "file376926767",
@@ -3001,6 +3001,89 @@
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text1337919823",
"max": 0,
"min": 0,
"name": "company",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2020362641",
"max": 0,
"min": 0,
"name": "taxId",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text922858135",
"max": 0,
"min": 0,
"name": "timezone",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text3571151285",
"max": 0,
"min": 0,
"name": "language",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text1767278655",
"max": 0,
"min": 0,
"name": "currency",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"cascadeDelete": false,
"collectionId": "pbc_1509025625",
"hidden": false,
"id": "relation2115670734",
"maxSelect": 999,
"minSelect": 0,
"name": "billingAddress",
"presentable": false,
"required": false,
"system": false,
"type": "relation"
}
],
"indexes": [],
@@ -3604,11 +3687,11 @@
},
{
"id": "pbc_1509025625",
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"listRule": "",
"viewRule": "",
"createRule": "",
"updateRule": "",
"deleteRule": "",
"name": "billingAddress",
"type": "base",
"fields": [
@@ -3798,11 +3881,11 @@
},
{
"id": "pbc_2109205374",
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"listRule": "",
"viewRule": "",
"createRule": "",
"updateRule": "",
"deleteRule": "",
"name": "t1",
"type": "base",
"fields": [

View File

@@ -2,7 +2,7 @@
tags: cms, login-flow
---
# login flow
# CMS login flow
## description

View File

@@ -0,0 +1,43 @@
---
tags: architecture,mobile, cms, db
---
# System architecture
## Description
it should have a family photo of used framework
## Diagram
```mermaid {align="center"}
architecture-beta
group running_config(logos:aws-opsworks)[running_config]
service db(database)[pocketbase] in running_config
service tra1(internet)[incoming traffic 3000] in running_config
service cms(logos:nextjs)[next app] in running_config
service tra2(internet)[incoming traffic 5173] in running_config
service ionic(logos:ionic)[ionic app] in running_config
tra1:R --> L:cms
cms:R --> L:db
tra2:R --> L:ionic
ionic:R --> B:db
%% group planning(logos:aws-lambda)[planning]
%% service api_ts(logos:aws-lambda)[api_ts] in planning
%% service pg_db(logos:postgresql)[pg_db] in planning
%% ui:R --> L:api_ts
%% ionic:R --> L:api_ts
%% api_ts:B --> T:pg_db
%% service task_server(logos:aws-lambda)[task_server] in planning
%% api_ts:R --> L:task_server
%% service marketing(logos:wordpress-icon)[marketing] in planning
```

View File

@@ -0,0 +1,37 @@
---
tags: mobile, login-flow
---
# Mobile login flow
## description
```mermaid
graph TD;
Start-->A;
A-->B;
B-->C;
B-->D;
D-->E;
E-->F;
C-->G;
G-->A
F-->End;
A[greeting, asking username and password]
B[check if username and password is valid]
C[pasword failed]
D[pasword ok]
E[login success]
F[redirect to '/dashboard']
G[prompt user wrong username and password]
Start((start));
End((end))
```
### relations
[REQ0016](../REQ0016/index.md)

View File

@@ -17,5 +17,8 @@
- [REQ0013: cms dashboard](./REQ0013/index.md)
- [REQ0014: mobile client](./REQ0014/index.md)
- [REQ0015: pocketbase json schema to dbml converter](./REQ0015/index.md)
- [REQ0016: login flow](./REQ0016/index.md)
- [REQ0016: CMS login flow](./REQ0016/index.md)
- [REQ0017: lesson page documentation](./REQ0017/index.md)
- [REQ0018: family photo of frameworks](./REQ0018/index.md)
- [REQ0019: System architecture](./REQ0019/index.md)
- [REQ0020: Mobile login flow](./REQ0020/index.md)

View File

@@ -28,7 +28,10 @@ const config = {
'',
'^[./]',
],
plugins: ['@ianvs/prettier-plugin-sort-imports'],
plugins: [
'@ianvs/prettier-plugin-sort-imports',
//
],
overrides: [
{
files: ['*.tsx'],

View File

@@ -1,19 +1,25 @@
'use client';
// src/app/dashboard/students/create/page.tsx
// PURPOSE
// T.B.A.
//
import * as React from 'react';
import type { Metadata } from 'next';
import RouterLink from 'next/link';
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
import { useTranslation } from 'react-i18next';
import { config } from '@/config';
import { paths } from '@/paths';
import { CustomerCreateForm } from '@/components/dashboard/student/student-create-form';
export const metadata = { title: `Create | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;
import { StudentCreateForm } from '@/components/dashboard/student/student-create-form';
export default function Page(): React.JSX.Element {
const { t } = useTranslation(['students']);
return (
<Box
sx={{
@@ -29,19 +35,22 @@ export default function Page(): React.JSX.Element {
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.customers.list}
href={paths.dashboard.students.list}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
Customers
{t('students')}
</Link>
</div>
<div>
<Typography variant="h4">Create customer</Typography>
<Typography variant="h4">
{t('create-student')}
{/* */}
</Typography>
</div>
</Stack>
<CustomerCreateForm />
<StudentCreateForm />
</Stack>
</Box>
);

View File

@@ -1 +0,0 @@
this `tsx` file is clone from elsewhere, please understand, modify and update the content of `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/students/edit/[customerId]/page.tsx.draft` to handle `Student` record thanks, modify comments/variables/paths/functions name please

View File

@@ -0,0 +1,6 @@
this `tsx` file is clone from elsewhere,
please understand, modify and update the content of
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/students/edit/[customerId]/page.tsx.draft`
to handle `Student` record thanks,
modify comments/variables/paths/functions name please

View File

@@ -1,6 +1,9 @@
'use client';
// src/app/dashboard/students/edit/[customerId]/page.tsx
// src/app/dashboard/students/edit/[id]/page.tsx
// PURPOSE
// T.B.A.
//
import * as React from 'react';
import RouterLink from 'next/link';
import Box from '@mui/material/Box';
@@ -11,7 +14,8 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { CrCategoryEditForm } from '@/components/dashboard/cr/categories/cr-category-edit-form';
// TODO: remove me
// import { CrCategoryEditForm } from '@/components/dashboard/cr/categories/cr-category-edit-form';
import { StudentEditForm } from '@/components/dashboard/student/student-edit-form';
export default function Page(): React.JSX.Element {

View File

@@ -1,4 +1,9 @@
'use client';
// src/app/dashboard/teachers/create/page.tsx
// PURPOSE
// T.B.A.
//
import * as React from 'react';
import RouterLink from 'next/link';
import Box from '@mui/material/Box';
@@ -6,12 +11,15 @@ import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
import { useTranslation } from 'react-i18next';
import { config } from '@/config';
import { paths } from '@/paths';
import { TeacherCreateForm } from '@/components/dashboard/teacher/teacher-create-form';
export default function Page(): React.JSX.Element {
const { t } = useTranslation(['teachers']);
return (
<Box
sx={{
@@ -32,11 +40,14 @@ export default function Page(): React.JSX.Element {
variant="subtitle2"
>
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
Teachers
{t('teachers')}
</Link>
</div>
<div>
<Typography variant="h4">Create teacher</Typography>
<Typography variant="h4">
{t('create-teacher')}
{/* */}
</Typography>
</div>
</Stack>
<TeacherCreateForm />

View File

@@ -1,5 +1,9 @@
'use client';
// src/app/dashboard/teachers/edit/[id]/page.tsx
// PURPOSE
// T.B.A.
//
import * as React from 'react';
import RouterLink from 'next/link';
import Box from '@mui/material/Box';
@@ -10,7 +14,8 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { CrCategoryEditForm } from '@/components/dashboard/cr/categories/cr-category-edit-form';
// TODO: remove me
// import { CrCategoryEditForm } from '@/components/dashboard/cr/categories/cr-category-edit-form';
import { TeacherEditForm } from '@/components/dashboard/teacher/teacher-edit-form';
export default function Page(): React.JSX.Element {

View File

@@ -1,216 +0,0 @@
// src/app/dashboard/teachers/list/page.tsx
'use client';
// RULES:
// contains list page for teachers (Teachers)
//
import * as React from 'react';
import { useRouter } from 'next/navigation';
import { COL_USER_METAS } from '@/constants';
import { LoadingButton } from '@mui/lab';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import Divider from '@mui/material/Divider';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
import type { ListResult, RecordModel } from 'pocketbase';
import { TeachersFilters } from '@/components/dashboard/teacher/teachers-filters';
import { TeachersPagination } from '@/components/dashboard/teacher/teachers-pagination';
import { TeachersSelectionProvider } from '@/components/dashboard/teacher/teachers-selection-context';
import { TeachersTable } from '@/components/dashboard/teacher/teachers-table';
import type { Teacher } from '@/components/dashboard/teacher/type.d';
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
import ErrorDisplay from '@/components/dashboard/error';
import { defaultTeacher } from '@/components/dashboard/teacher/_constants';
import FormLoading from '@/components/loading';
export default function Page({ searchParams }: PageProps): React.JSX.Element {
const { t } = useTranslation(['teachers']);
const router = useRouter();
const { email, phone, sortDir, status } = searchParams;
const [teacherData, setTeacherData] = React.useState<Teacher[]>([]);
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
const [showLoading, setShowLoading] = React.useState<boolean>(true);
const [showError, setShowError] = React.useState({ show: false, detail: '' });
//
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
//
const [f, setF] = React.useState<Teacher[]>([]);
const [currentPage, setCurrentPage] = React.useState<number>(0);
//
const [recordCount, setRecordCount] = React.useState<number>(0);
const [listOption, setListOption] = React.useState({ filter: '' });
const [listSort, setListSort] = React.useState({});
function isListOptionChanged() {
return JSON.stringify(listOption) === '{}';
}
//
const reloadRows = async (): Promise<void> => {
try {
const listOptionTeacherOnly = isListOptionChanged()
? { filter: `role = "teacher"` }
: { filter: [listOption.filter, `role = "teacher"`].join(' && ') };
const models: ListResult<RecordModel> = await pb
.collection(COL_USER_METAS)
.getList(currentPage + 1, rowsPerPage, listOptionTeacherOnly);
const { items, totalItems } = models;
const tempTeacher: Teacher[] = items.map((lt) => {
return { ...defaultTeacher, ...lt };
});
setTeacherData(tempTeacher);
setRecordCount(totalItems);
setF(tempTeacher);
} catch (error) {
logger.error(error);
setShowError({
//
show: true,
detail: JSON.stringify(error, null, 2),
});
} finally {
setShowLoading(false);
}
};
const [lastListOption, setLastListOption] = React.useState({});
const isFirstRun = React.useRef(false);
React.useEffect(() => {
if (!isFirstRun.current) {
isFirstRun.current = true;
} else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
// reset page number as tab changes
setLastListOption(listOption);
setCurrentPage(0);
void reloadRows();
} else {
void reloadRows();
}
}, [currentPage, rowsPerPage, listOption]);
React.useEffect(() => {
const tempFilter = [];
let tempSortDir = '';
if (status) {
tempFilter.push(`status = "${status}"`);
}
if (sortDir) {
tempSortDir = `-created`;
}
if (email) {
tempFilter.push(`email ~ "%${email}%"`);
}
if (phone) {
tempFilter.push(`phone ~ "%${phone}%"`);
}
let preFinalListOption = { filter: '' };
if (tempFilter.length > 0) {
preFinalListOption = { filter: tempFilter.join(' && ') };
}
if (tempSortDir.length > 0) {
preFinalListOption = { ...preFinalListOption, sort: tempSortDir };
}
setListOption(preFinalListOption);
}, [sortDir, email, phone, status]);
if (showLoading) return <FormLoading />;
if (showError.show)
return (
<ErrorDisplay
message={t('error.unable-to-process-request')}
code={-1}
details={showError.detail}
/>
);
return (
<Box
sx={{
maxWidth: 'var(--Content-maxWidth)',
m: 'var(--Content-margin)',
p: 'var(--Content-padding)',
width: 'var(--Content-width)',
}}
>
<Stack spacing={4}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={3}
sx={{ alignItems: 'flex-start' }}
>
<Box sx={{ flex: '1 1 auto' }}>
<Typography variant="h4">{t('list.title')}</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<LoadingButton
loading={isLoadingAddPage}
onClick={(): void => {
setIsLoadingAddPage(true);
router.push(paths.dashboard.teachers.create);
}}
startIcon={<PlusIcon />}
variant="contained"
>
{t('list.add')}
</LoadingButton>
</Box>
</Stack>
<TeachersSelectionProvider teachers={f}>
<Card>
<TeachersFilters
filters={{ email, phone, status }}
fullData={teacherData}
sortDir={sortDir}
/>
<Divider />
<Box sx={{ overflowX: 'auto' }}>
<TeachersTable
reloadRows={reloadRows}
rows={f}
/>
</Box>
<Divider />
<TeachersPagination
count={recordCount}
page={currentPage}
rowsPerPage={rowsPerPage}
setPage={setCurrentPage}
setRowsPerPage={setRowsPerPage}
/>
</Card>
</TeachersSelectionProvider>
</Stack>
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
<pre>{JSON.stringify(f, null, 2)}</pre>
</Box>
</Box>
);
}
interface PageProps {
searchParams: {
email?: string;
phone?: string;
sortDir?: 'asc' | 'desc';
status?: string;
//
};
}

View File

@@ -1,3 +0,0 @@
this `tsx` file is clone from elsewhere, please understand, modify and update the content of `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/user_metas/edit/[id]/page.tsx.draft` to handle `UserMeta` record thanks, modify comments/variables/paths/functions name please
e.g. why `lessonCategories` still exist ?

View File

@@ -1,8 +1,9 @@
'use client';
// src/app/dashboard/user_metas/edit/[id]/page.tsx
// src/app/dashboard/user_metas/edit/[id]/page.tsx
import * as React from 'react';
import RouterLink from 'next/link';
import { useParams } from 'next/navigation';
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
@@ -11,11 +12,12 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { CrCategoryEditForm } from '@/components/dashboard/cr/categories/cr-category-edit-form';
import { UserActivationEditForm } from '@/components/dashboard/user_meta/user-activation-edit-form';
import { UserMetaEditForm } from '@/components/dashboard/user_meta/user-meta-edit-form';
export default function Page(): React.JSX.Element {
const { t } = useTranslation(['lp_categories']);
const { t } = useTranslation(['user_metas']);
const { id: userId } = useParams<{ id: string }>();
React.useEffect(() => {
// console.log('helloworld');
@@ -49,6 +51,7 @@ export default function Page(): React.JSX.Element {
</div>
</Stack>
<UserMetaEditForm />
<UserActivationEditForm userId={userId} />
</Stack>
</Box>
);

View File

@@ -15,12 +15,6 @@ import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
import type { ListResult, RecordModel } from 'pocketbase';
import { UserMetasFilters } from '@/components/dashboard/user_meta/user-metas-filters';
import { UserMetasPagination } from '@/components/dashboard/user_meta/user-metas-pagination';
import { UserMetasSelectionProvider } from '@/components/dashboard/user_meta/user-metas-selection-context';
import { UserMetasTable } from '@/components/dashboard/user_meta/user-metas-table';
import type { UserMeta } from '@/components/dashboard/user_meta/type.d';
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
@@ -29,10 +23,15 @@ import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
import ErrorDisplay from '@/components/dashboard/error';
import { defaultUserMeta } from '@/components/dashboard/user_meta/_constants';
import type { UserMeta } from '@/components/dashboard/user_meta/type.d';
import { UserMetasFilters } from '@/components/dashboard/user_meta/user-metas-filters';
import { UserMetasPagination } from '@/components/dashboard/user_meta/user-metas-pagination';
import { UserMetasSelectionProvider } from '@/components/dashboard/user_meta/user-metas-selection-context';
import { UserMetasTable } from '@/components/dashboard/user_meta/user-metas-table';
import FormLoading from '@/components/loading';
export default function Page({ searchParams }: PageProps): React.JSX.Element {
const { t } = useTranslation(['teachers']);
const { t } = useTranslation(['user_metas']);
const router = useRouter();
const { email, phone, sortDir, status } = searchParams;

View File

@@ -14,7 +14,7 @@ import { useTranslation } from 'react-i18next';
import { PropertyItem } from '@/components/core/property-item';
import { PropertyList } from '@/components/core/property-list';
// import { CrCategory } from '@/components/dashboard/cr/categories/type';
import type { UserMeta } from '@/components/dashboard/user_meta/type.d';
import type { UserMeta } from '@/components/dashboard/user_meta/type_move.d';
export default function BasicDetailCard({
userMeta,

View File

@@ -1,5 +1,9 @@
'use client';
// src/app/dashboard/user_metas/view/[id]/page.tsx
// PURPOSE
// T.B.A.
//
import * as React from 'react';
import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
@@ -7,7 +11,7 @@ import SampleAddressCard from '@/app/dashboard/Sample/AddressCard';
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard';
import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard';
import { COL_USER_METAS } from '@/constants';
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
@@ -21,16 +25,14 @@ import { paths } from '@/paths';
import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
import { toast } from '@/components/core/toaster';
import ErrorDisplay from '@/components/dashboard/error';
import { defaultUserMeta } from '@/components/dashboard/user_meta/_constants';
import { Notifications } from '@/components/dashboard/user_meta/notifications';
import type { UserMeta } from '@/components/dashboard/user_meta/type_move.d';
import FormLoading from '@/components/loading';
import BasicDetailCard from './BasicDetailCard';
import TitleCard from './TitleCard';
import { defaultUserMeta } from '@/components/dashboard/user_meta/_constants';
import type { UserMeta } from '@/components/dashboard/user_meta/type.d';
import { COL_USER_METAS } from '@/constants';
export default function Page(): React.JSX.Element {
const { t } = useTranslation();

View File

@@ -1,3 +1,6 @@
// PURPOSE
// T.B.A.
//
const helloworld = 'helloworld';
export { helloworld };

View File

@@ -1,9 +1,16 @@
'use client';
// src/components/dashboard/student/student-create-form.tsx
// PURPOSE
// T.B.A.
//
import * as React from 'react';
import RouterLink from 'next/link';
import { useRouter } from 'next/navigation';
import { createStudent } from '@/db/Students/Create';
import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab';
//
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
@@ -16,41 +23,34 @@ 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 MenuItem from '@mui/material/MenuItem';
import OutlinedInput from '@mui/material/OutlinedInput';
import Select from '@mui/material/Select';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Unstable_Grid2';
//
import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera';
//
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import { logger } from '@/lib/default-logger';
import { Option } from '@/components/core/option';
import { toast } from '@/components/core/toaster';
import { createCustomer } from '@/db/Customers/Create';
import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger';
import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
import { toast } from '@/components/core/toaster';
function fileToBase64(file: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
resolve(reader.result as string);
};
reader.onerror = () => {
reject(new Error('Error converting file to base64'));
};
});
}
// import ErrorDisplay from '../../error';
import { CreateFormProps } from './type.d';
// TODO: review schema
const schema = zod.object({
avatar: zod.string().optional(),
name: zod.string().min(1, 'Name is required').max(255),
email: zod.string().email('Must be a valid email').min(1, 'Email is required').max(255),
phone: zod.string().min(1, 'Phone is required').max(15),
company: zod.string().max(255),
phone: zod.string().min(1, 'Phone is required').max(25),
company: zod.string().max(255).optional(),
billingAddress: zod.object({
country: zod.string().min(1, 'Country is required').max(255),
state: zod.string().min(1, 'State is required').max(255),
@@ -63,12 +63,12 @@ const schema = zod.object({
timezone: zod.string().min(1, 'Timezone is required').max(255),
language: zod.string().min(1, 'Language is required').max(255),
currency: zod.string().min(1, 'Currency is required').max(255),
avatar: zod.string().optional(),
});
type Values = zod.infer<typeof schema>;
const defaultValues = {
avatar: '',
name: 'new name',
email: '123@123.com',
phone: '91234567',
@@ -85,10 +85,18 @@ const defaultValues = {
timezone: 'new_york',
language: 'en',
currency: 'USD',
avatar: '',
} satisfies Values;
export function CustomerCreateForm(): React.JSX.Element {
export function StudentCreateForm(): React.JSX.Element {
const router = useRouter();
const { t } = useTranslation(['students']);
//
const [isUpdating, setIsUpdating] = React.useState<boolean>(false);
const [showLoading, setShowLoading] = React.useState<boolean>(false);
//
const [showError, setShowError] = React.useState({ show: false, detail: '' });
const {
control,
@@ -100,14 +108,35 @@ export function CustomerCreateForm(): React.JSX.Element {
const onSubmit = React.useCallback(
async (values: Values): Promise<void> => {
try {
// Use standard create method from db/Customers/Create
const record = await createCustomer(values);
toast.success('Customer created');
const tempCreate: CreateFormProps = {
avatar: values.avatar ? await base64ToFile(values.avatar) : null,
//
name: values.name,
email: values.email,
phone: values.phone,
company: values.company,
timezone: values.timezone,
language: values.language,
currency: values.currency,
taxId: values.taxId,
state: 'pending',
meta: {},
};
try {
// if (billingAddressId) {
// await UpdateBillingAddressById(billingAddressId, values.billingAddress);
// }
const record = await createStudent(tempCreate);
toast.success('student-created');
router.push(paths.dashboard.students.view(record.id));
} catch (err) {
logger.error(err);
toast.error('Failed to create customer');
toast.error('failed-to-create-student');
} finally {
setIsUpdating(false);
}
},
[router]
@@ -137,7 +166,7 @@ export function CustomerCreateForm(): React.JSX.Element {
spacing={4}
>
<Stack spacing={3}>
<Typography variant="h6">Account information</Typography>
<Typography variant="h6">{t('create.basic-info')}</Typography>
<Grid
container
spacing={3}
@@ -151,12 +180,13 @@ export function CustomerCreateForm(): React.JSX.Element {
<Box
sx={{
border: '1px dashed var(--mui-palette-divider)',
borderRadius: '50%',
borderRadius: '5%',
display: 'inline-flex',
p: '4px',
}}
>
<Avatar
variant="rounded"
src={avatar}
sx={{
'--Avatar-size': '100px',
@@ -175,8 +205,8 @@ export function CustomerCreateForm(): React.JSX.Element {
spacing={1}
sx={{ alignItems: 'flex-start' }}
>
<Typography variant="subtitle1">Avatar</Typography>
<Typography variant="caption">Min 400x400px, PNG or JPEG</Typography>
<Typography variant="subtitle1">{t('create.avatar')}</Typography>
<Typography variant="caption">{t('create.avatarRequirements')}</Typography>
<Button
color="secondary"
onClick={() => {
@@ -184,7 +214,7 @@ export function CustomerCreateForm(): React.JSX.Element {
}}
variant="outlined"
>
Select
{t('create.avatar_select')}
</Button>
<input
hidden
@@ -226,7 +256,7 @@ export function CustomerCreateForm(): React.JSX.Element {
error={Boolean(errors.email)}
fullWidth
>
<InputLabel required>Email address</InputLabel>
<InputLabel required>{t('create.email-address')}</InputLabel>
<OutlinedInput
{...field}
type="email"
@@ -248,7 +278,7 @@ export function CustomerCreateForm(): React.JSX.Element {
error={Boolean(errors.phone)}
fullWidth
>
<InputLabel required>Phone number</InputLabel>
<InputLabel required>{t('create.phone-number')}</InputLabel>
<OutlinedInput {...field} />
{errors.phone ? <FormHelperText>{errors.phone.message}</FormHelperText> : null}
</FormControl>
@@ -268,7 +298,10 @@ export function CustomerCreateForm(): React.JSX.Element {
fullWidth
>
<InputLabel>Company</InputLabel>
<OutlinedInput {...field} />
<OutlinedInput
{...field}
placeholder="no company name"
/>
{errors.company ? <FormHelperText>{errors.company.message}</FormHelperText> : null}
</FormControl>
)}
@@ -276,8 +309,9 @@ export function CustomerCreateForm(): React.JSX.Element {
</Grid>
</Grid>
</Stack>
{/* */}
<Stack spacing={3}>
<Typography variant="h6">Billing information</Typography>
<Typography variant="h6">{t('create.billing-information')}</Typography>
<Grid
container
spacing={3}
@@ -296,10 +330,12 @@ export function CustomerCreateForm(): React.JSX.Element {
>
<InputLabel required>Country</InputLabel>
<Select {...field}>
<Option value="">Choose a country</Option>
<Option value="us">United States</Option>
<Option value="de">Germany</Option>
<Option value="es">Spain</Option>
<MenuItem value="">Choose a country</MenuItem>
<MenuItem value="US">United States</MenuItem>
<MenuItem value="UK">United Kingdom</MenuItem>
<MenuItem value="CA">Canada</MenuItem>
<MenuItem value="DE">Germany</MenuItem>
<MenuItem value="ES">Spain</MenuItem>
</Select>
{errors.billingAddress?.country ? (
<FormHelperText>{errors.billingAddress?.country?.message}</FormHelperText>
@@ -362,7 +398,7 @@ export function CustomerCreateForm(): React.JSX.Element {
error={Boolean(errors.billingAddress?.zipCode)}
fullWidth
>
<InputLabel required>Zip code</InputLabel>
<InputLabel required>{t('create.zip-code')}</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.zipCode ? (
<FormHelperText>{errors.billingAddress?.zipCode?.message}</FormHelperText>
@@ -383,7 +419,7 @@ export function CustomerCreateForm(): React.JSX.Element {
error={Boolean(errors.billingAddress?.line1)}
fullWidth
>
<InputLabel required>Address</InputLabel>
<InputLabel required>{t('create.address-line-1')}</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.line1 ? (
<FormHelperText>{errors.billingAddress?.line1?.message}</FormHelperText>
@@ -424,7 +460,7 @@ export function CustomerCreateForm(): React.JSX.Element {
/>
</Stack>
<Stack spacing={3}>
<Typography variant="h6">Additional information</Typography>
<Typography variant="h6">{t('create.additional-information')}</Typography>
<Grid
container
spacing={3}
@@ -443,10 +479,14 @@ export function CustomerCreateForm(): React.JSX.Element {
>
<InputLabel required>Timezone</InputLabel>
<Select {...field}>
<Option value="">Select a timezone</Option>
<Option value="new_york">US - New York</Option>
<Option value="california">US - California</Option>
<Option value="london">UK - London</Option>
<MenuItem value="">Select a timezone</MenuItem>
<MenuItem value="Europe/London">London</MenuItem>
<MenuItem value="Asia/Tokyo">Tokyo</MenuItem>
<MenuItem value="America/Boa_Vista">Boa Vista</MenuItem>
<MenuItem value="America/Grand_Turk">Grand Turk</MenuItem>
<MenuItem value="Asia/Manila">Manila</MenuItem>
<MenuItem value="Asia/Urumqi">Urumqi</MenuItem>
<MenuItem value="Africa/Tunis">Tunis</MenuItem>
</Select>
{errors.timezone ? <FormHelperText>{errors.timezone.message}</FormHelperText> : null}
</FormControl>
@@ -467,10 +507,11 @@ export function CustomerCreateForm(): React.JSX.Element {
>
<InputLabel required>Language</InputLabel>
<Select {...field}>
<Option value="">Select a language</Option>
<Option value="en">English</Option>
<Option value="es">Spanish</Option>
<Option value="de">German</Option>
<MenuItem value="">Select a language</MenuItem>
<MenuItem value="en">English</MenuItem>
<MenuItem value="es">Spanish</MenuItem>
<MenuItem value="de">German</MenuItem>
<MenuItem value="fr">French</MenuItem>
</Select>
{errors.language ? <FormHelperText>{errors.language.message}</FormHelperText> : null}
</FormControl>
@@ -489,12 +530,12 @@ export function CustomerCreateForm(): React.JSX.Element {
error={Boolean(errors.currency)}
fullWidth
>
<InputLabel>Currency</InputLabel>
<InputLabel required>{t('create.currency')}</InputLabel>
<Select {...field}>
<Option value="">Select a currency</Option>
<Option value="USD">USD</Option>
<Option value="EUR">EUR</Option>
<Option value="RON">RON</Option>
<MenuItem value="">no currency selected</MenuItem>
<MenuItem value="USD">USD</MenuItem>
<MenuItem value="EUR">EUR</MenuItem>
<MenuItem value="GBP">GBP</MenuItem>
</Select>
{errors.currency ? <FormHelperText>{errors.currency.message}</FormHelperText> : null}
</FormControl>
@@ -511,14 +552,17 @@ export function CustomerCreateForm(): React.JSX.Element {
component={RouterLink}
href={paths.dashboard.students.list}
>
Cancel
{t('create.cancelButton')}
</Button>
<Button
<LoadingButton
disabled={isUpdating}
loading={isUpdating}
type="submit"
variant="contained"
>
Create customer
</Button>
{t('create.updateButton')}
</LoadingButton>
</CardActions>
</Card>
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>

View File

@@ -1,10 +1,16 @@
'use client';
// src/components/dashboard/student/student-edit-form.tsx
// PURPOSE:
// handle change details for student collection
//
import * as React from 'react';
import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
//
import { COL_CUSTOMERS } from '@/constants';
import { UpdateBillingAddressById } from '@/db/billingAddress/UpdateById';
import { getStudentById } from '@/db/Students/GetById';
import { UpdateStudentById } from '@/db/Students/UpdateById';
import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab';
//
@@ -35,12 +41,13 @@ import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger';
import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
import { pb } from '@/lib/pb';
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
import { toast } from '@/components/core/toaster';
import FormLoading from '@/components/loading';
// import ErrorDisplay from '../../error';
import ErrorDisplay from '../error';
import type { Student } from './type.d';
// TODO: review schema
const schema = zod.object({
@@ -87,14 +94,15 @@ const defaultValues = {
export function StudentEditForm(): React.JSX.Element {
const router = useRouter();
const { t } = useTranslation(['lp_categories']);
const { t } = useTranslation(['students']);
const { customerId } = useParams<{ customerId: string }>();
const { id: studentId } = useParams<{ 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 [billingAddressId, setBillingAddressId] = React.useState<string | null>(null);
const {
control,
@@ -114,7 +122,9 @@ export function StudentEditForm(): React.JSX.Element {
email: values.email,
phone: values.phone,
company: values.company,
billingAddress: values.billingAddress,
//
// billingAddress: values.billingAddress,
//
taxId: values.taxId,
timezone: values.timezone,
language: values.language,
@@ -123,17 +133,22 @@ export function StudentEditForm(): React.JSX.Element {
};
try {
await pb.collection(COL_CUSTOMERS).update(customerId, updateData);
toast.success('Customer updated successfully');
await UpdateStudentById(studentId, updateData);
//
toast.success(t('student-updated-successfully'));
router.push(paths.dashboard.students.list);
if (billingAddressId) {
await UpdateBillingAddressById(billingAddressId, values.billingAddress);
}
} catch (error) {
logger.error(error);
toast.error('Failed to update customer');
toast.error(t('failed-to-update-student'));
} finally {
setIsUpdating(false);
}
},
[customerId, router]
[studentId, router]
);
const avatarInputRef = React.useRef<HTMLInputElement>(null);
@@ -162,21 +177,21 @@ export function StudentEditForm(): React.JSX.Element {
setShowLoading(true);
try {
const result = await pb.collection(COL_CUSTOMERS).getOne(id);
const result = (await getStudentById(id)) as unknown as Student;
//
reset({ ...defaultValues, ...result });
console.log({ result });
if (result.avatar_file) {
const fetchResult = await fetch(
`http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.avatar_file}`
);
setBillingAddressId(result.billingAddress.id);
if (result.avatar) {
const fetchResult = await fetch(getImageUrlFromFile(result.collectionId, result.id, result.avatar));
const blob = await fetchResult.blob();
const url = await fileToBase64(blob);
setValue('avatar', url);
}
} catch (error) {
logger.error(error);
toast.error('Failed to load customer data');
toast.error(t('failed-to-load-student-data'));
setShowError({ show: true, detail: JSON.stringify(error, null, 2) });
} finally {
setShowLoading(false);
@@ -186,9 +201,9 @@ export function StudentEditForm(): React.JSX.Element {
);
React.useEffect(() => {
void loadExistingData(customerId);
void loadExistingData(studentId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [customerId]);
}, [studentId]);
if (showLoading) return <FormLoading />;
if (showError.show)
@@ -299,7 +314,7 @@ export function StudentEditForm(): React.JSX.Element {
error={Boolean(errors.email)}
fullWidth
>
<InputLabel required>Email</InputLabel>
<InputLabel required>{t('edit.email-address')}</InputLabel>
<OutlinedInput
{...field}
type="email"
@@ -321,7 +336,7 @@ export function StudentEditForm(): React.JSX.Element {
error={Boolean(errors.phone)}
fullWidth
>
<InputLabel required>Phone</InputLabel>
<InputLabel required>{t('edit.phone-number')}</InputLabel>
<OutlinedInput {...field} />
{errors.phone ? <FormHelperText>{errors.phone.message}</FormHelperText> : null}
</FormControl>
@@ -350,11 +365,12 @@ export function StudentEditForm(): React.JSX.Element {
)}
/>
</Grid>
{/* */}
</Grid>
</Stack>
{/* */}
<Stack spacing={3}>
<Typography variant="h6">Billing Information</Typography>
<Typography variant="h6">{t('edit.billing-information')}</Typography>
<Grid
container
spacing={3}
@@ -373,9 +389,12 @@ export function StudentEditForm(): React.JSX.Element {
>
<InputLabel required>Country</InputLabel>
<Select {...field}>
<MenuItem value="">No Country selected</MenuItem>
<MenuItem value="US">United States</MenuItem>
<MenuItem value="UK">United Kingdom</MenuItem>
<MenuItem value="CA">Canada</MenuItem>
<MenuItem value="DE">Germany</MenuItem>
<MenuItem value="ES">Spain</MenuItem>
</Select>
{errors.billingAddress?.country ? (
<FormHelperText>{errors.billingAddress.country.message}</FormHelperText>
@@ -438,7 +457,7 @@ export function StudentEditForm(): React.JSX.Element {
error={Boolean(errors.billingAddress?.zipCode)}
fullWidth
>
<InputLabel required>Zip Code</InputLabel>
<InputLabel required>{t('edit.zip-code')}</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.zipCode ? (
<FormHelperText>{errors.billingAddress.zipCode.message}</FormHelperText>
@@ -459,7 +478,7 @@ export function StudentEditForm(): React.JSX.Element {
error={Boolean(errors.billingAddress?.line1)}
fullWidth
>
<InputLabel required>Address Line 1</InputLabel>
<InputLabel required>{t('edit.address-line-1')}</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.line1 ? (
<FormHelperText>{errors.billingAddress.line1.message}</FormHelperText>
@@ -494,7 +513,7 @@ export function StudentEditForm(): React.JSX.Element {
</Stack>
<Stack spacing={3}>
<Typography variant="h6">Additional Information</Typography>
<Typography variant="h6">{t('edit.additional-information')}</Typography>
<Grid
container
spacing={3}
@@ -541,8 +560,10 @@ export function StudentEditForm(): React.JSX.Element {
>
<InputLabel required>Language</InputLabel>
<Select {...field}>
<MenuItem value="">no language selected</MenuItem>
<MenuItem value="en">English</MenuItem>
<MenuItem value="es">Spanish</MenuItem>
<MenuItem value="de">German</MenuItem>
<MenuItem value="fr">French</MenuItem>
</Select>
{errors.language ? <FormHelperText>{errors.language.message}</FormHelperText> : null}
@@ -562,8 +583,9 @@ export function StudentEditForm(): React.JSX.Element {
error={Boolean(errors.currency)}
fullWidth
>
<InputLabel required>Currency</InputLabel>
<InputLabel required>{t('edit.currency')}</InputLabel>
<Select {...field}>
<MenuItem value="">no currency selected</MenuItem>
<MenuItem value="USD">USD</MenuItem>
<MenuItem value="EUR">EUR</MenuItem>
<MenuItem value="GBP">GBP</MenuItem>

View File

@@ -1,20 +1,67 @@
'use client';
// src/components/dashboard/student/type.d.tsx
// RULES: sorting direction for student lists
import type { BillingAddress } from '@/db/billingAddress/type';
// RULES: sorting direction for teacher lists
export type SortDir = 'asc' | 'desc';
export interface DBStudent {
name: string;
//
// NOTE: obslete "avatar" and use "avatar_file"
avatar?: string;
avatar_file?: string;
//
email: string;
phone: string;
quota: number;
company: string;
//
// billingAddress: BillingAddress[] | [];
expand: { billingAddress?: BillingAddress[] };
// status is obsoleted, replace by state
status: 'pending' | 'active' | 'blocked';
state: 'pending' | 'active' | 'blocked';
//
timezone: string;
language: string;
currency: string;
//
id: string;
created: string;
updated?: string;
collectionId: string;
}
// RULES: core student data structure
export interface Student {
id: string;
collectionId: string;
name: string;
//
// NOTE: obslete "avatar" and use "avatar_file"
avatar?: string;
avatar_file?: string;
//
email: string;
phone?: string;
quota: number;
company?: string;
//
billingAddress: BillingAddress | Record<string, never>;
// status is obsoleted, replace by state
status: 'pending' | 'active' | 'blocked';
state: 'pending' | 'active' | 'blocked';
//
timezone: string;
language: string;
currency: string;
//
id: string;
createdAt: Date;
updatedAt?: Date;
collectionId: string;
}
// RULES: form data structure for creating new student
@@ -23,21 +70,25 @@ export interface CreateFormProps {
email: string;
phone?: string;
company?: string;
billingAddress?: {
country: string;
state: string;
city: string;
zipCode: string;
line1: string;
line2?: string;
};
//
// handle seperately
// billingAddress?: {
// country: string;
// state: string;
// city: string;
// zipCode: string;
// line1: string;
// line2?: string;
// };
//
taxId?: string;
timezone: string;
language: string;
currency: string;
avatar?: string;
avatar?: File | null;
// quota?: number;
// status?: 'pending' | 'active' | 'blocked';
state?: 'pending' | 'active' | 'blocked';
meta: Record<string, null>;
}
// RULES: form data structure for editing existing student
@@ -62,6 +113,7 @@ export interface EditFormProps {
// quota?: number;
// status?: 'pending' | 'active' | 'blocked';
}
// RULES: filter props for student search and filtering
export interface CustomersFiltersProps {
filters?: Filters;

View File

@@ -1,9 +1,19 @@
'use client';
// src/components/dashboard/teacher/teacher-create-form.tsx
// PURPOSE
// T.B.A.
//
import * as React from 'react';
import RouterLink from 'next/link';
import { useRouter } from 'next/navigation';
import { UpdateBillingAddressById } from '@/db/billingAddress/UpdateById';
import { createTeacher } from '@/db/Teachers/Create';
import { getTeacherById } from '@/db/Teachers/GetById';
import { UpdateTeacherById } from '@/db/Teachers/UpdateById';
import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab';
//
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
@@ -16,41 +26,38 @@ 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 MenuItem from '@mui/material/MenuItem';
import OutlinedInput from '@mui/material/OutlinedInput';
import Select from '@mui/material/Select';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Unstable_Grid2';
//
import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera';
//
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development';
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 { toast } from '@/components/core/toaster';
import { createTeacher } from '@/db/Teachers/Create';
import isDevelopment from '@/lib/check-is-development';
import FormLoading from '@/components/loading';
function fileToBase64(file: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
resolve(reader.result as string);
};
reader.onerror = () => {
reject(new Error('Error converting file to base64'));
};
});
}
// import ErrorDisplay from '../../error';
import ErrorDisplay from '../error';
import { CreateFormProps } from './type.d';
// TODO: review schema
const schema = zod.object({
avatar: zod.string().optional(),
name: zod.string().min(1, 'Name is required').max(255),
email: zod.string().email('Must be a valid email').min(1, 'Email is required').max(255),
phone: zod.string().min(1, 'Phone is required').max(15),
company: zod.string().max(255),
phone: zod.string().min(1, 'Phone is required').max(25),
company: zod.string().max(255).optional(),
billingAddress: zod.object({
country: zod.string().min(1, 'Country is required').max(255),
state: zod.string().min(1, 'State is required').max(255),
@@ -63,12 +70,12 @@ const schema = zod.object({
timezone: zod.string().min(1, 'Timezone is required').max(255),
language: zod.string().min(1, 'Language is required').max(255),
currency: zod.string().min(1, 'Currency is required').max(255),
avatar: zod.string().optional(),
});
type Values = zod.infer<typeof schema>;
const defaultValues = {
avatar: '',
name: 'new name',
email: '123@123.com',
phone: '91234567',
@@ -85,10 +92,18 @@ const defaultValues = {
timezone: 'new_york',
language: 'en',
currency: 'USD',
avatar: '',
} satisfies Values;
export function TeacherCreateForm(): React.JSX.Element {
const router = useRouter();
const { t } = useTranslation(['students']);
//
const [isUpdating, setIsUpdating] = React.useState<boolean>(false);
const [showLoading, setShowLoading] = React.useState<boolean>(false);
//
const [showError, setShowError] = React.useState({ show: false, detail: '' });
const {
control,
@@ -100,14 +115,31 @@ export function TeacherCreateForm(): React.JSX.Element {
const onSubmit = React.useCallback(
async (values: Values): Promise<void> => {
try {
// Use standard create method from db/Customers/Create
const record = await createTeacher(values);
toast.success('Customer created');
const tempCreate: CreateFormProps = {
avatar: values.avatar ? await base64ToFile(values.avatar) : null,
//
name: values.name,
email: values.email,
phone: values.phone,
company: values.company,
timezone: values.timezone,
language: values.language,
currency: values.currency,
taxId: values.taxId,
state: 'pending',
meta: {},
};
try {
const record = await createTeacher(tempCreate);
toast.success('teacher-created');
router.push(paths.dashboard.teachers.details(record.id));
} catch (err) {
logger.error(err);
toast.error('Failed to create customer');
toast.error('failed-to-create-teacher');
} finally {
setIsUpdating(false);
}
},
[router]
@@ -137,7 +169,7 @@ export function TeacherCreateForm(): React.JSX.Element {
spacing={4}
>
<Stack spacing={3}>
<Typography variant="h6">Account information</Typography>
<Typography variant="h6">{t('create.basic-info')}</Typography>
<Grid
container
spacing={3}
@@ -151,12 +183,13 @@ export function TeacherCreateForm(): React.JSX.Element {
<Box
sx={{
border: '1px dashed var(--mui-palette-divider)',
borderRadius: '50%',
borderRadius: '5%',
display: 'inline-flex',
p: '4px',
}}
>
<Avatar
variant="rounded"
src={avatar}
sx={{
'--Avatar-size': '100px',
@@ -175,8 +208,8 @@ export function TeacherCreateForm(): React.JSX.Element {
spacing={1}
sx={{ alignItems: 'flex-start' }}
>
<Typography variant="subtitle1">Avatar</Typography>
<Typography variant="caption">Min 400x400px, PNG or JPEG</Typography>
<Typography variant="subtitle1">{t('create.avatar')}</Typography>
<Typography variant="caption">{t('create.avatarRequirements')}</Typography>
<Button
color="secondary"
onClick={() => {
@@ -184,7 +217,7 @@ export function TeacherCreateForm(): React.JSX.Element {
}}
variant="outlined"
>
Select
{t('create.avatar_select')}
</Button>
<input
hidden
@@ -226,7 +259,7 @@ export function TeacherCreateForm(): React.JSX.Element {
error={Boolean(errors.email)}
fullWidth
>
<InputLabel required>Email address</InputLabel>
<InputLabel required>{t('create.email-address')}</InputLabel>
<OutlinedInput
{...field}
type="email"
@@ -248,7 +281,7 @@ export function TeacherCreateForm(): React.JSX.Element {
error={Boolean(errors.phone)}
fullWidth
>
<InputLabel required>Phone number</InputLabel>
<InputLabel required>{t('create.phone-number')}</InputLabel>
<OutlinedInput {...field} />
{errors.phone ? <FormHelperText>{errors.phone.message}</FormHelperText> : null}
</FormControl>
@@ -268,7 +301,10 @@ export function TeacherCreateForm(): React.JSX.Element {
fullWidth
>
<InputLabel>Company</InputLabel>
<OutlinedInput {...field} />
<OutlinedInput
{...field}
placeholder="no company name"
/>
{errors.company ? <FormHelperText>{errors.company.message}</FormHelperText> : null}
</FormControl>
)}
@@ -276,8 +312,9 @@ export function TeacherCreateForm(): React.JSX.Element {
</Grid>
</Grid>
</Stack>
{/* */}
<Stack spacing={3}>
<Typography variant="h6">Billing information</Typography>
<Typography variant="h6">{t('create.billing-information')}</Typography>
<Grid
container
spacing={3}
@@ -296,10 +333,12 @@ export function TeacherCreateForm(): React.JSX.Element {
>
<InputLabel required>Country</InputLabel>
<Select {...field}>
<Option value="">Choose a country</Option>
<Option value="us">United States</Option>
<Option value="de">Germany</Option>
<Option value="es">Spain</Option>
<MenuItem value="">Choose a country</MenuItem>
<MenuItem value="US">United States</MenuItem>
<MenuItem value="UK">United Kingdom</MenuItem>
<MenuItem value="CA">Canada</MenuItem>
<MenuItem value="DE">Germany</MenuItem>
<MenuItem value="ES">Spain</MenuItem>
</Select>
{errors.billingAddress?.country ? (
<FormHelperText>{errors.billingAddress?.country?.message}</FormHelperText>
@@ -362,7 +401,7 @@ export function TeacherCreateForm(): React.JSX.Element {
error={Boolean(errors.billingAddress?.zipCode)}
fullWidth
>
<InputLabel required>Zip code</InputLabel>
<InputLabel required>{t('create.zip-code')}</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.zipCode ? (
<FormHelperText>{errors.billingAddress?.zipCode?.message}</FormHelperText>
@@ -383,7 +422,7 @@ export function TeacherCreateForm(): React.JSX.Element {
error={Boolean(errors.billingAddress?.line1)}
fullWidth
>
<InputLabel required>Address</InputLabel>
<InputLabel required>{t('create.address-line-1')}</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.line1 ? (
<FormHelperText>{errors.billingAddress?.line1?.message}</FormHelperText>
@@ -424,7 +463,7 @@ export function TeacherCreateForm(): React.JSX.Element {
/>
</Stack>
<Stack spacing={3}>
<Typography variant="h6">Additional information</Typography>
<Typography variant="h6">{t('create.additional-information')}</Typography>
<Grid
container
spacing={3}
@@ -443,10 +482,14 @@ export function TeacherCreateForm(): React.JSX.Element {
>
<InputLabel required>Timezone</InputLabel>
<Select {...field}>
<Option value="">Select a timezone</Option>
<Option value="new_york">US - New York</Option>
<Option value="california">US - California</Option>
<Option value="london">UK - London</Option>
<MenuItem value="">Select a timezone</MenuItem>
<MenuItem value="Europe/London">London</MenuItem>
<MenuItem value="Asia/Tokyo">Tokyo</MenuItem>
<MenuItem value="America/Boa_Vista">Boa Vista</MenuItem>
<MenuItem value="America/Grand_Turk">Grand Turk</MenuItem>
<MenuItem value="Asia/Manila">Manila</MenuItem>
<MenuItem value="Asia/Urumqi">Urumqi</MenuItem>
<MenuItem value="Africa/Tunis">Tunis</MenuItem>
</Select>
{errors.timezone ? <FormHelperText>{errors.timezone.message}</FormHelperText> : null}
</FormControl>
@@ -467,10 +510,11 @@ export function TeacherCreateForm(): React.JSX.Element {
>
<InputLabel required>Language</InputLabel>
<Select {...field}>
<Option value="">Select a language</Option>
<Option value="en">English</Option>
<Option value="es">Spanish</Option>
<Option value="de">German</Option>
<MenuItem value="">Select a language</MenuItem>
<MenuItem value="en">English</MenuItem>
<MenuItem value="es">Spanish</MenuItem>
<MenuItem value="de">German</MenuItem>
<MenuItem value="fr">French</MenuItem>
</Select>
{errors.language ? <FormHelperText>{errors.language.message}</FormHelperText> : null}
</FormControl>
@@ -489,12 +533,12 @@ export function TeacherCreateForm(): React.JSX.Element {
error={Boolean(errors.currency)}
fullWidth
>
<InputLabel>Currency</InputLabel>
<InputLabel required>{t('create.currency')}</InputLabel>
<Select {...field}>
<Option value="">Select a currency</Option>
<Option value="USD">USD</Option>
<Option value="EUR">EUR</Option>
<Option value="RON">RON</Option>
<MenuItem value="">no currency selected</MenuItem>
<MenuItem value="USD">USD</MenuItem>
<MenuItem value="EUR">EUR</MenuItem>
<MenuItem value="GBP">GBP</MenuItem>
</Select>
{errors.currency ? <FormHelperText>{errors.currency.message}</FormHelperText> : null}
</FormControl>
@@ -511,14 +555,17 @@ export function TeacherCreateForm(): React.JSX.Element {
component={RouterLink}
href={paths.dashboard.teachers.list}
>
Cancel
{t('create.cancelButton')}
</Button>
<Button
<LoadingButton
disabled={isUpdating}
loading={isUpdating}
type="submit"
variant="contained"
>
Create teacher
</Button>
{t('create.updateButton')}
</LoadingButton>
</CardActions>
</Card>
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>

View File

@@ -1,10 +1,16 @@
'use client';
// src/components/dashboard/teacher/teacher-edit-form.tsx
// PURPOSE:
// handle change details for teachers collection
//
import * as React from 'react';
import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
//
import { COL_TEACHERS, COL_USER_METAS } from '@/constants';
import { UpdateBillingAddressById } from '@/db/billingAddress/UpdateById';
import { getTeacherById } from '@/db/Teachers/GetById';
import { UpdateTeacherById } from '@/db/Teachers/UpdateById';
import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab';
//
@@ -32,17 +38,18 @@ import { useTranslation } from 'react-i18next';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger';
import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
import { pb } from '@/lib/pb';
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
import { toast } from '@/components/core/toaster';
import FormLoading from '@/components/loading';
// import ErrorDisplay from '../../error';
import ErrorDisplay from '../error';
import isDevelopment from '@/lib/check-is-development';
import type { Teacher } from './type.d';
// TODO: review this
// TODO: review schema
const schema = zod.object({
name: zod.string().min(1, 'Name is required').max(255),
email: zod.string().email('Must be a valid email').min(1, 'Email is required').max(255),
@@ -87,7 +94,7 @@ const defaultValues = {
export function TeacherEditForm(): React.JSX.Element {
const router = useRouter();
const { t } = useTranslation(['lp_categories']);
const { t } = useTranslation(['teachers']);
const { id: teacherId } = useParams<{ id: string }>();
//
@@ -95,6 +102,7 @@ export function TeacherEditForm(): React.JSX.Element {
const [showLoading, setShowLoading] = React.useState<boolean>(false);
//
const [showError, setShowError] = React.useState({ show: false, detail: '' });
const [billingAddressId, setBillingAddressId] = React.useState<string | null>(null);
const {
control,
@@ -114,7 +122,9 @@ export function TeacherEditForm(): React.JSX.Element {
email: values.email,
phone: values.phone,
company: values.company,
billingAddress: values.billingAddress,
//
// billingAddress: values.billingAddress,
//
taxId: values.taxId,
timezone: values.timezone,
language: values.language,
@@ -123,12 +133,17 @@ export function TeacherEditForm(): React.JSX.Element {
};
try {
await pb.collection(COL_USER_METAS).update(teacherId, updateData);
toast.success('Teacher updated successfully');
await UpdateTeacherById(teacherId, updateData);
//
toast.success(t('teacher-updated-successfully'));
router.push(paths.dashboard.teachers.list);
if (billingAddressId) {
await UpdateBillingAddressById(billingAddressId, values.billingAddress);
}
} catch (error) {
logger.error(error);
toast.error('Failed to update teacher');
toast.error(t('failed-to-update-teacher'));
} finally {
setIsUpdating(false);
}
@@ -162,21 +177,21 @@ export function TeacherEditForm(): React.JSX.Element {
setShowLoading(true);
try {
const result = await pb.collection(COL_USER_METAS).getOne(id);
const result = (await getTeacherById(id)) as unknown as Teacher;
//
reset({ ...defaultValues, ...result });
console.log({ result });
setBillingAddressId(result.billingAddress.id);
if (result.avatar) {
const fetchResult = await fetch(
`http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.avatar}`
);
const fetchResult = await fetch(getImageUrlFromFile(result.collectionId, result.id, result.avatar));
const blob = await fetchResult.blob();
const url = await fileToBase64(blob);
setValue('avatar', url);
}
} catch (error) {
logger.error(error);
toast.error('Failed to load teacher data');
toast.error(t('failed-to-load-teacher-data'));
setShowError({ show: true, detail: JSON.stringify(error, null, 2) });
} finally {
setShowLoading(false);
@@ -299,7 +314,7 @@ export function TeacherEditForm(): React.JSX.Element {
error={Boolean(errors.email)}
fullWidth
>
<InputLabel required>Email</InputLabel>
<InputLabel required>{t('edit.email-address')}</InputLabel>
<OutlinedInput
{...field}
type="email"
@@ -321,7 +336,7 @@ export function TeacherEditForm(): React.JSX.Element {
error={Boolean(errors.phone)}
fullWidth
>
<InputLabel required>Phone</InputLabel>
<InputLabel required>{t('edit.phone-number')}</InputLabel>
<OutlinedInput {...field} />
{errors.phone ? <FormHelperText>{errors.phone.message}</FormHelperText> : null}
</FormControl>
@@ -350,11 +365,12 @@ export function TeacherEditForm(): React.JSX.Element {
)}
/>
</Grid>
{/* */}
</Grid>
</Stack>
{/* */}
<Stack spacing={3}>
<Typography variant="h6">Billing Information</Typography>
<Typography variant="h6">{t('edit.billing-information')}</Typography>
<Grid
container
spacing={3}
@@ -373,9 +389,12 @@ export function TeacherEditForm(): React.JSX.Element {
>
<InputLabel required>Country</InputLabel>
<Select {...field}>
<MenuItem value="">No Country selected</MenuItem>
<MenuItem value="US">United States</MenuItem>
<MenuItem value="UK">United Kingdom</MenuItem>
<MenuItem value="CA">Canada</MenuItem>
<MenuItem value="DE">Germany</MenuItem>
<MenuItem value="ES">Spain</MenuItem>
</Select>
{errors.billingAddress?.country ? (
<FormHelperText>{errors.billingAddress.country.message}</FormHelperText>
@@ -438,7 +457,7 @@ export function TeacherEditForm(): React.JSX.Element {
error={Boolean(errors.billingAddress?.zipCode)}
fullWidth
>
<InputLabel required>Zip Code</InputLabel>
<InputLabel required>{t('edit.zip-code')}</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.zipCode ? (
<FormHelperText>{errors.billingAddress.zipCode.message}</FormHelperText>
@@ -459,7 +478,7 @@ export function TeacherEditForm(): React.JSX.Element {
error={Boolean(errors.billingAddress?.line1)}
fullWidth
>
<InputLabel required>Address Line 1</InputLabel>
<InputLabel required>{t('edit.address-line-1')}</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.line1 ? (
<FormHelperText>{errors.billingAddress.line1.message}</FormHelperText>
@@ -494,7 +513,7 @@ export function TeacherEditForm(): React.JSX.Element {
</Stack>
<Stack spacing={3}>
<Typography variant="h6">Additional Information</Typography>
<Typography variant="h6">{t('edit.additional-information')}</Typography>
<Grid
container
spacing={3}
@@ -541,8 +560,10 @@ export function TeacherEditForm(): React.JSX.Element {
>
<InputLabel required>Language</InputLabel>
<Select {...field}>
<MenuItem value="">no language selected</MenuItem>
<MenuItem value="en">English</MenuItem>
<MenuItem value="es">Spanish</MenuItem>
<MenuItem value="de">German</MenuItem>
<MenuItem value="fr">French</MenuItem>
</Select>
{errors.language ? <FormHelperText>{errors.language.message}</FormHelperText> : null}
@@ -562,8 +583,9 @@ export function TeacherEditForm(): React.JSX.Element {
error={Boolean(errors.currency)}
fullWidth
>
<InputLabel required>Currency</InputLabel>
<InputLabel required>{t('edit.currency')}</InputLabel>
<Select {...field}>
<MenuItem value="">no currency selected</MenuItem>
<MenuItem value="USD">USD</MenuItem>
<MenuItem value="EUR">EUR</MenuItem>
<MenuItem value="GBP">GBP</MenuItem>

View File

@@ -1,5 +1,9 @@
'use client';
// src/components/dashboard/teacher/teachers-table.tsx
// PURPOSE:
// handle change details for teachers collection
//
import * as React from 'react';
import RouterLink from 'next/link';
import { LoadingButton } from '@mui/lab';
@@ -213,7 +217,6 @@ export function TeachersTable({ rows, reloadRows }: TeachersTableProps): React.J
sx={{ textAlign: 'center' }}
variant="body2"
>
{/* TODO: update this */}
{t('no-teachers-found')}
</Typography>
</Box>

View File

@@ -31,21 +31,23 @@ export interface CreateFormProps {
email: string;
phone?: string;
company?: string;
billingAddress?: {
country: string;
state: string;
city: string;
zipCode: string;
line1: string;
line2?: string;
};
// handle seperately
// billingAddress?: {
// country: string;
// state: string;
// city: string;
// zipCode: string;
// line1: string;
// line2?: string;
// };
taxId?: string;
timezone: string;
language: string;
currency: string;
avatar?: string;
avatar?: File | null;
// quota?: number;
// status?: 'pending' | 'active' | 'blocked';
state?: 'pending' | 'active' | 'blocked';
meta: Record<string, null>;
}
// RULES: form data structure for editing existing teacher
@@ -77,6 +79,8 @@ export interface TeachersFiltersProps {
sortDir?: SortDir;
fullData: Teacher[];
}
// RULES: available filter options for student data
export interface Filters {
email?: string;
phone?: string;

View File

@@ -32,6 +32,7 @@ import { useTranslation } from 'react-i18next';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger';
import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
import { pb } from '@/lib/pb';
@@ -40,7 +41,6 @@ import FormLoading from '@/components/loading';
// import ErrorDisplay from '../../error';
import ErrorDisplay from '../error';
import isDevelopment from '@/lib/check-is-development';
// TODO: review this
const schema = zod.object({

View File

@@ -3,6 +3,7 @@
// empty valur for customer
import { dayjs } from '@/lib/dayjs';
import type { UserMeta } from './type.d';
export const defaultUserMeta: UserMeta = {

View File

@@ -1,9 +1,41 @@
'use client';
// src/components/dashboard/user_meta/type.d.tsx
// RULES: sorting direction for user meta lists
import type { BillingAddress } from '@/db/billingAddress/type';
// RULES: sorting direction for teacher lists
export type SortDir = 'asc' | 'desc';
// RULES: core teacher data structure
export interface DBUserMeta {
name: string;
//
// NOTE: obslete "avatar" and use "avatar_file"
avatar?: string;
avatar_file?: string;
//
email: string;
phone: string;
quota: number;
company: string;
//
// billingAddress: BillingAddress[] | [];
expand: { billingAddress?: BillingAddress[] };
// status is obsoleted, replace by state
status: 'pending' | 'active' | 'blocked';
state: 'pending' | 'active' | 'blocked';
//
timezone: string;
language: string;
currency: string;
//
id: string;
created: string;
updated?: string;
collectionId: string;
}
// RULES: core user meta data structure
export interface UserMeta {
name: string;
//
@@ -14,23 +46,32 @@ export interface UserMeta {
email: string;
phone?: string;
quota: number;
company?: string;
//
billingAddress: BillingAddress | Record<string, never>;
// status is obsoleted, replace by state
status: 'pending' | 'active' | 'blocked';
state: 'pending' | 'active' | 'blocked';
//
timezone: string;
language: string;
currency: string;
//
id: string;
createdAt: Date;
updatedAt?: Date;
collectionId: string;
}
// RULES: form data structure for creating new teacher
// RULES: form data structure for creating new user meta
export interface CreateFormProps {
name: string;
email: string;
phone?: string;
company?: string;
//
// handle seperately ?
billingAddress?: {
country: string;
state: string;
@@ -39,6 +80,7 @@ export interface CreateFormProps {
line1: string;
line2?: string;
};
//
taxId?: string;
timezone: string;
language: string;
@@ -48,7 +90,7 @@ export interface CreateFormProps {
// status?: 'pending' | 'active' | 'blocked';
}
// RULES: form data structure for editing existing teacher
// RULES: form data structure for editing existing user meta
export interface EditFormProps {
name: string;
email: string;
@@ -71,12 +113,13 @@ export interface EditFormProps {
// status?: 'pending' | 'active' | 'blocked';
}
// RULES: filter props for teacher search and filtering
// RULES: filter props for user meta search and filtering
export interface UserMetasFiltersProps {
filters?: Filters;
sortDir?: SortDir;
fullData: UserMeta[];
}
// RULES: available filter options for user meta data
export interface Filters {
email?: string;
phone?: string;

View File

@@ -0,0 +1,193 @@
'use client';
//
// src/components/dashboard/user_meta/user-activation-edit-form.tsx
// RULES
// handle user change activation of other users
//
import * as React from 'react';
import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
//
import { COL_USERS } from '@/constants';
import { getUserById } from '@/db/Users/GetById';
import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab';
//
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardActions from '@mui/material/CardActions';
import CardContent from '@mui/material/CardContent';
import Divider from '@mui/material/Divider';
import FormControl from '@mui/material/FormControl';
import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select';
import Stack from '@mui/material/Stack';
//
//
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
import { toast } from '@/components/core/toaster';
import FormLoading from '@/components/loading';
// import ErrorDisplay from '../../error';
import ErrorDisplay from '../error';
// TODO: review this
const schema = zod.object({
verified: zod.string(),
});
type Values = zod.infer<typeof schema>;
const defaultValues = {
verified: 'false',
} satisfies Values;
export function UserActivationEditForm({ userId }: { userId: string }): React.JSX.Element {
const router = useRouter();
const { t } = useTranslation(['user_metas']);
const { id: userMetaId } = useParams<{ 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 updateData = {
verified: false,
};
try {
await pb.collection(COL_USERS).update(userId, updateData);
toast.success(t('user-updated-successfully'));
// router.push(paths.dashboard.user_metas.list);
} catch (error) {
logger.error(error);
toast.error(t('failed-to-update-user-meta'));
} finally {
setIsUpdating(false);
}
},
[userMetaId, router]
);
// TODO: need to align with save form
// use trycatch
const [textDescription, setTextDescription] = React.useState<string>('');
const [textRemarks, setTextRemarks] = React.useState<string>('');
// load existing data when user arrive
const loadExistingData = React.useCallback(
async (id: string) => {
try {
const result = await getUserById(userId);
reset({ verified: result.verified.toString() });
setShowLoading(false);
} catch (error) {
logger.error(error);
toast.error('failed-to-load-user-meta-data');
setShowError({ show: true, detail: JSON.stringify(error, null, 2) });
} finally {
setShowLoading(false);
}
},
[reset, setValue]
);
React.useEffect(() => {
void loadExistingData(userId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId]);
if (showLoading) return <FormLoading />;
if (showError.show)
return (
<ErrorDisplay
message={t('error.unable-to-process-request')}
code="500"
details={showError.detail}
/>
);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Card>
<CardContent>
<Stack
divider={<Divider />}
spacing={4}
>
<Controller
control={control}
name="verified"
render={({ field }) => (
<FormControl
error={Boolean(errors.verified)}
fullWidth
>
<InputLabel required>
{t('user-activation')} {t('optional')}
</InputLabel>
<Select {...field}>
<MenuItem value="true">{t('activated')}</MenuItem>
<MenuItem value="false">{t('not-actviate')}</MenuItem>
</Select>
{errors.verified ? <FormHelperText>{errors.verified.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Stack>
</CardContent>
<CardActions sx={{ justifyContent: 'flex-end' }}>
<div>
<Button
color="secondary"
component={RouterLink}
href={paths.dashboard.user_metas.list}
>
{t('edit.cancelButton')}
</Button>
<LoadingButton
disabled={isUpdating}
loading={isUpdating}
type="submit"
variant="contained"
>
{t('edit.updateButton')}
</LoadingButton>
</div>
</CardActions>
</Card>
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
<pre>{JSON.stringify({ errors }, null, 2)}</pre>
</Box>
</form>
);
}

View File

@@ -1,10 +1,16 @@
'use client';
// src/components/dashboard/user_meta/user-meta-edit-form.tsx
// PURPOSE:
// handle change details for user meta collection
//
import * as React from 'react';
import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
//
import { COL_TEACHERS, COL_USER_METAS } from '@/constants';
import { UpdateBillingAddressById } from '@/db/billingAddress/UpdateById';
import { getUserMetaById } from '@/db/UserMetas/GetById';
import { UpdateUserMetaById } from '@/db/UserMetas/UpdateById';
import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab';
//
@@ -32,17 +38,18 @@ import { useTranslation } from 'react-i18next';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger';
import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
import { pb } from '@/lib/pb';
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
import { toast } from '@/components/core/toaster';
import FormLoading from '@/components/loading';
// import ErrorDisplay from '../../error';
import ErrorDisplay from '../error';
import isDevelopment from '@/lib/check-is-development';
import type { UserMeta } from './type.d';
// TODO: review this
// TODO: review schema
const schema = zod.object({
name: zod.string().min(1, 'Name is required').max(255),
email: zod.string().email('Must be a valid email').min(1, 'Email is required').max(255),
@@ -87,14 +94,15 @@ const defaultValues = {
export function UserMetaEditForm(): React.JSX.Element {
const router = useRouter();
const { t } = useTranslation(['lp_categories']);
const { t } = useTranslation(['user_metas']);
const { id: teacherId } = useParams<{ id: string }>();
const { id: userMetaId } = useParams<{ 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 [billingAddressId, setBillingAddressId] = React.useState<string | null>(null);
const {
control,
@@ -114,7 +122,9 @@ export function UserMetaEditForm(): React.JSX.Element {
email: values.email,
phone: values.phone,
company: values.company,
billingAddress: values.billingAddress,
//
// billingAddress: values.billingAddress,
//
taxId: values.taxId,
timezone: values.timezone,
language: values.language,
@@ -123,17 +133,22 @@ export function UserMetaEditForm(): React.JSX.Element {
};
try {
await pb.collection(COL_USER_METAS).update(teacherId, updateData);
toast.success('Teacher updated successfully');
router.push(paths.dashboard.teachers.list);
await UpdateUserMetaById(userMetaId, updateData);
//
toast.success(t('user-updated-successfully'));
router.push(paths.dashboard.user_metas.list);
if (billingAddressId) {
await UpdateBillingAddressById(billingAddressId, values.billingAddress);
}
} catch (error) {
logger.error(error);
toast.error('Failed to update teacher');
toast.error(t('failed-to-update-user-meta'));
} finally {
setIsUpdating(false);
}
},
[teacherId, router]
[userMetaId, router]
);
const avatarInputRef = React.useRef<HTMLInputElement>(null);
@@ -162,21 +177,21 @@ export function UserMetaEditForm(): React.JSX.Element {
setShowLoading(true);
try {
const result = await pb.collection(COL_USER_METAS).getOne(id);
const result = (await getUserMetaById(id)) as unknown as UserMeta;
//
reset({ ...defaultValues, ...result });
console.log({ result });
setBillingAddressId(result.billingAddress.id);
if (result.avatar) {
const fetchResult = await fetch(
`http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.avatar}`
);
const fetchResult = await fetch(getImageUrlFromFile(result.collectionId, result.id, result.avatar));
const blob = await fetchResult.blob();
const url = await fileToBase64(blob);
setValue('avatar', url);
}
} catch (error) {
logger.error(error);
toast.error('Failed to load teacher data');
toast.error(t('failed-to-load-user-meta-data'));
setShowError({ show: true, detail: JSON.stringify(error, null, 2) });
} finally {
setShowLoading(false);
@@ -186,9 +201,9 @@ export function UserMetaEditForm(): React.JSX.Element {
);
React.useEffect(() => {
void loadExistingData(teacherId);
void loadExistingData(userMetaId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [teacherId]);
}, [userMetaId]);
if (showLoading) return <FormLoading />;
if (showError.show)
@@ -299,7 +314,7 @@ export function UserMetaEditForm(): React.JSX.Element {
error={Boolean(errors.email)}
fullWidth
>
<InputLabel required>Email</InputLabel>
<InputLabel required>{t('edit.email-address')}</InputLabel>
<OutlinedInput
{...field}
type="email"
@@ -321,7 +336,7 @@ export function UserMetaEditForm(): React.JSX.Element {
error={Boolean(errors.phone)}
fullWidth
>
<InputLabel required>Phone</InputLabel>
<InputLabel required>{t('edit.phone-number')}</InputLabel>
<OutlinedInput {...field} />
{errors.phone ? <FormHelperText>{errors.phone.message}</FormHelperText> : null}
</FormControl>
@@ -350,11 +365,12 @@ export function UserMetaEditForm(): React.JSX.Element {
)}
/>
</Grid>
{/* */}
</Grid>
</Stack>
{/* */}
<Stack spacing={3}>
<Typography variant="h6">Billing Information</Typography>
<Typography variant="h6">{t('edit.billing-information')}</Typography>
<Grid
container
spacing={3}
@@ -373,9 +389,12 @@ export function UserMetaEditForm(): React.JSX.Element {
>
<InputLabel required>Country</InputLabel>
<Select {...field}>
<MenuItem value="">No Country selected</MenuItem>
<MenuItem value="US">United States</MenuItem>
<MenuItem value="UK">United Kingdom</MenuItem>
<MenuItem value="CA">Canada</MenuItem>
<MenuItem value="DE">Germany</MenuItem>
<MenuItem value="ES">Spain</MenuItem>
</Select>
{errors.billingAddress?.country ? (
<FormHelperText>{errors.billingAddress.country.message}</FormHelperText>
@@ -438,7 +457,7 @@ export function UserMetaEditForm(): React.JSX.Element {
error={Boolean(errors.billingAddress?.zipCode)}
fullWidth
>
<InputLabel required>Zip Code</InputLabel>
<InputLabel required>{t('edit.zip-code')}</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.zipCode ? (
<FormHelperText>{errors.billingAddress.zipCode.message}</FormHelperText>
@@ -459,7 +478,7 @@ export function UserMetaEditForm(): React.JSX.Element {
error={Boolean(errors.billingAddress?.line1)}
fullWidth
>
<InputLabel required>Address Line 1</InputLabel>
<InputLabel required>{t('edit.address-line-1')}</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.line1 ? (
<FormHelperText>{errors.billingAddress.line1.message}</FormHelperText>
@@ -494,7 +513,7 @@ export function UserMetaEditForm(): React.JSX.Element {
</Stack>
<Stack spacing={3}>
<Typography variant="h6">Additional Information</Typography>
<Typography variant="h6">{t('edit.additional-information')}</Typography>
<Grid
container
spacing={3}
@@ -541,8 +560,10 @@ export function UserMetaEditForm(): React.JSX.Element {
>
<InputLabel required>Language</InputLabel>
<Select {...field}>
<MenuItem value="">no language selected</MenuItem>
<MenuItem value="en">English</MenuItem>
<MenuItem value="es">Spanish</MenuItem>
<MenuItem value="de">German</MenuItem>
<MenuItem value="fr">French</MenuItem>
</Select>
{errors.language ? <FormHelperText>{errors.language.message}</FormHelperText> : null}
@@ -562,8 +583,9 @@ export function UserMetaEditForm(): React.JSX.Element {
error={Boolean(errors.currency)}
fullWidth
>
<InputLabel required>Currency</InputLabel>
<InputLabel required>{t('edit.currency')}</InputLabel>
<Select {...field}>
<MenuItem value="">no currency selected</MenuItem>
<MenuItem value="USD">USD</MenuItem>
<MenuItem value="EUR">EUR</MenuItem>
<MenuItem value="GBP">GBP</MenuItem>
@@ -581,7 +603,7 @@ export function UserMetaEditForm(): React.JSX.Element {
<Button
color="secondary"
component={RouterLink}
href={paths.dashboard.teachers.list}
href={paths.dashboard.user_metas.list}
>
{t('edit.cancelButton')}
</Button>

View File

@@ -1,11 +1,15 @@
'use client';
// src/components/dashboard/user_meta/user-metas-filters.tsx
// RULES:
// T.B.A.
//
import * as React from 'react';
import { useRouter } from 'next/navigation';
import GetActiveCount from '@/db/UserMetas/GetActiveCount';
import getAllUserMetasCount from '@/db/UserMetas/GetAllCount';
import GetBlockedCount from '@/db/UserMetas/GetBlockedCount';
import GetPendingCount from '@/db/UserMetas/GetPendingCount';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
import Divider from '@mui/material/Divider';
@@ -18,17 +22,14 @@ import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { logger } from '@/lib/default-logger';
import { FilterButton } from '@/components/core/filter-button';
import { Option } from '@/components/core/option';
import { useUserMetasSelection } from './user-metas-selection-context';
import GetBlockedCount from '@/db/UserMetas/GetBlockedCount';
import GetPendingCount from '@/db/UserMetas/GetPendingCount';
import GetActiveCount from '@/db/UserMetas/GetActiveCount';
import PhoneFilterPopover from './phone-filter-popover';
import EmailFilterPopover from './email-filter-popover';
import type { UserMetasFiltersProps, Filters, SortDir } from './type.d';
import { logger } from '@/lib/default-logger';
import PhoneFilterPopover from './phone-filter-popover';
import type { Filters, SortDir, UserMetasFiltersProps } from './type.d';
import { useUserMetasSelection } from './user-metas-selection-context';
export function UserMetasFilters({
filters = {},

View File

@@ -1,5 +1,9 @@
'use client';
// src/components/dashboard/user_meta/user-metas-table.tsx
// RULES:
// T.B.A.
//
import * as React from 'react';
import RouterLink from 'next/link';
import { LoadingButton } from '@mui/lab';
@@ -18,14 +22,16 @@ import { Images as ImagesIcon } from '@phosphor-icons/react/dist/ssr/Images';
import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus';
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
import { TrashSimple as TrashSimpleIcon } from '@phosphor-icons/react/dist/ssr/TrashSimple';
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { dayjs } from '@/lib/dayjs';
import { DataTable } from '@/components/core/data-table';
import type { ColumnDef } from '@/components/core/data-table';
import ConfirmDeleteModal from './confirm-delete-modal';
import { useUserMetasSelection } from './user-metas-selection-context';
import type { UserMeta } from './type.d';
import { useUserMetasSelection } from './user-metas-selection-context';
function columns(handleDeleteClick: (userMetaId: string) => void): ColumnDef<UserMeta>[] {
return [
@@ -168,6 +174,7 @@ export interface UserMetasTableProps {
}
export function UserMetasTable({ rows, reloadRows }: UserMetasTableProps): React.JSX.Element {
const { t } = useTranslation(['user_metas']);
const { deselectAll, deselectOne, selectAll, selectOne, selected } = useUserMetasSelection();
const [idToDelete, setIdToDelete] = React.useState('');
@@ -207,7 +214,7 @@ export function UserMetasTable({ rows, reloadRows }: UserMetasTableProps): React
sx={{ textAlign: 'center' }}
variant="body2"
>
No user metadata found
{t('no-user-meta-found')}
</Typography>
</Box>
) : null}

View File

@@ -5,6 +5,7 @@ const COL_LESSON_TYPES = 'LessonsTypes';
const COL_LESSON_CATEGORIES = 'LessonsCategories';
const COL_USERS = 'users';
const COL_USER_METAS = 'UserMetas';
const COL_BILLING_ADDRESS = 'billingAddress';
// RULES:
// do not use LP_CATEGORIES anymore
@@ -56,4 +57,5 @@ export {
COL_VOCABULARIES,
NS_VOCABULARY,
//
COL_BILLING_ADDRESS,
};

View File

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

View File

@@ -1,7 +1,32 @@
import { pb } from '@/lib/pb';
import { COL_STUDENTS } from '@/constants';
import { RecordModel } from 'pocketbase';
// src/db/Students/GetById.tsx
//
import { COL_USER_METAS } from '@/constants';
export async function getStudentById(id: string): Promise<RecordModel> {
return pb.collection(COL_STUDENTS).getOne(id);
import { pb } from '@/lib/pb';
import type { DBStudent, Student } from '@/components/dashboard/student/type';
export async function getStudentById(id: string): Promise<Student> {
const record = await pb
.collection(COL_USER_METAS)
.getOne<DBStudent>(id, { expand: 'billingAddress, helloworld', requestKey: null });
const temp: Student = {
id: record.id,
name: record.name,
email: record.email,
quota: record.quota,
billingAddress: record.expand.billingAddress ? record.expand.billingAddress[0] : {},
status: record.status,
state: record.state,
createdAt: new Date(record.created),
collectionId: record.collectionId,
avatar: record.avatar,
phone: record.phone,
company: record.company,
timezone: record.timezone,
language: record.language,
currency: record.currency,
};
return temp;
}

View File

@@ -1,3 +1,7 @@
// src/db/Students/Helloworld.tsx
// RULES:
// T.B.A.
//
export function helloCustomer() {
return 'Hello from Customers module!';
}

View File

@@ -0,0 +1,10 @@
import { COL_USER_METAS } from '@/constants';
import type { RecordModel } from 'pocketbase';
import { pb } from '@/lib/pb';
import type { UpdateStudent } from './type';
export async function UpdateStudentById(id: string, data: Partial<UpdateStudent>): Promise<RecordModel> {
return pb.collection(COL_USER_METAS).update(id, data);
}

View File

@@ -1,11 +1,96 @@
// src/db/Students/type.d.ts
//
// PURPOSE
// type for student record
//
// RULES: sorting direction for user meta lists
import type { BillingAddress } from '../billingAddress/type';
// Student type definitions
export interface Student {
id: string;
export interface DBStudentOld {
//
name: string;
avatar: string;
//
// NOTE: obslete "avatar" and use "avatar_file"
avatar?: string;
avatar_file?: string;
//
email: string;
phone: string;
quota: number;
status: 'active' | 'blocked' | 'pending';
createdAt: Date;
company: string;
//
// billingAddress: BillingAddress[] | [];
expand: { billingAddress?: BillingAddress[] };
// status is obsoleted, replace by state
status: 'pending' | 'active' | 'blocked';
state: 'pending' | 'active' | 'blocked';
//
timezone: string;
language: string;
currency: string;
//
id: string;
created: string;
updated?: string;
collectionId: string;
}
// RULES: core user meta data structure
export interface Student {
id: string;
name: string;
//
// NOTE: obslete "avatar" and use "avatar_file"
avatar?: string;
avatar_file?: string;
//
email: string;
phone?: string;
quota: number;
company?: string;
//
billingAddress: BillingAddress | Record<string, never>;
// status is obsoleted, replace by state
status: 'pending' | 'active' | 'blocked';
state: 'pending' | 'active' | 'blocked';
//
timezone: string;
language: string;
currency: string;
//
id: string;
createdAt: Date;
updatedAt?: Date;
collectionId: string;
}
export interface UpdateStudent {
name?: string;
//
// NOTE: obslete "avatar" and use "avatar_file"
// avatar_file?: string;
avatar: File | null;
//
email?: string;
phone?: string;
quota?: number;
company?: string;
//
// relation handle seperately
// billingAddress: BillingAddress | Record<string, never>;
// status is obsoleted, replace by state
// status: 'pending' | 'active' | 'blocked';
state?: 'pending' | 'active' | 'blocked';
//
timezone?: string;
language?: string;
currency?: string;
//
taxId?: string;
}

View File

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

View File

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

View File

@@ -1,7 +1,30 @@
import { pb } from '@/lib/pb';
import { COL_TEACHERS } from '@/constants';
import { RecordModel } from 'pocketbase';
// src/db/Teachers/GetById.tsx
//
import { COL_USER_METAS } from '@/constants';
export async function getTeacherById(id: string): Promise<RecordModel> {
return pb.collection(COL_TEACHERS).getOne(id);
import { pb } from '@/lib/pb';
import type { DBUserMeta, UserMeta } from '@/components/dashboard/user_meta/type.d';
export async function getTeacherById(id: string): Promise<UserMeta> {
const record = await pb.collection(COL_USER_METAS).getOne<DBUserMeta>(id, { expand: 'billingAddress, helloworld' });
const temp: UserMeta = {
id: record.id,
name: record.name,
email: record.email,
quota: record.quota,
billingAddress: record.expand.billingAddress ? record.expand.billingAddress[0] : {},
status: record.status,
state: record.state,
createdAt: new Date(record.created),
collectionId: record.collectionId,
avatar: record.avatar,
phone: record.phone,
company: record.company,
timezone: record.timezone,
language: record.language,
currency: record.currency,
};
return temp;
}

View File

@@ -0,0 +1,10 @@
import { COL_USER_METAS } from '@/constants';
import type { RecordModel } from 'pocketbase';
import { pb } from '@/lib/pb';
import type { UpdateTeacher } from './type';
export async function UpdateTeacherById(id: string, data: Partial<UpdateTeacher>): Promise<RecordModel> {
return pb.collection(COL_USER_METAS).update(id, data);
}

View File

@@ -0,0 +1,41 @@
//
// RULES
// type for teacher record
// Teacher type definitions
export interface Teacher {
id: string;
name: string;
avatar: string;
email: string;
phone: string;
quota: number;
status: 'active' | 'blocked' | 'pending';
createdAt: Date;
}
export interface UpdateTeacher {
name?: string;
//
// NOTE: obslete "avatar" and use "avatar_file"
// avatar_file?: string;
avatar: File | null;
//
email?: string;
phone?: string;
quota?: number;
company?: string;
//
// relation handle seperately
// billingAddress: BillingAddress | Record<string, never>;
// status is obsoleted, replace by state
// status: 'pending' | 'active' | 'blocked';
state?: 'pending' | 'active' | 'blocked';
//
timezone?: string;
language?: string;
currency?: string;
//
taxId?: string;
}

View File

@@ -1,11 +1,14 @@
// api method for crate customer record
// RULES:
// TBA
import { pb } from '@/lib/pb';
// src/db/UserMetas/Create.tsx
//
// PURPOSE:
// create user meta
//
import { COL_USER_METAS } from '@/constants';
import type { CreateFormProps } from '@/components/dashboard/user_meta/type.d';
import type { RecordModel } from 'pocketbase';
import { pb } from '@/lib/pb';
import type { CreateFormProps } from '@/components/dashboard/user_meta/type.d';
export async function createUserMeta(data: CreateFormProps): Promise<RecordModel> {
return pb.collection(COL_USER_METAS).create(data);
}

View File

@@ -1,7 +1,32 @@
import { pb } from '@/lib/pb';
// src/db/UserMetas/GetById.tsx
//
import { COL_USER_METAS } from '@/constants';
import { RecordModel } from 'pocketbase';
export async function getUserMetaById(id: string): Promise<RecordModel> {
return pb.collection(COL_USER_METAS).getOne(id);
import { pb } from '@/lib/pb';
import type { DBUserMeta, UserMeta } from '@/components/dashboard/user_meta/type';
export async function getUserMetaById(id: string): Promise<UserMeta> {
const record = await pb
.collection(COL_USER_METAS)
.getOne<DBUserMeta>(id, { expand: 'billingAddress, helloworld', requestKey: null });
const temp: UserMeta = {
id: record.id,
name: record.name,
email: record.email,
quota: record.quota,
billingAddress: record.expand.billingAddress ? record.expand.billingAddress[0] : {},
status: record.status,
state: record.state,
createdAt: new Date(record.created),
collectionId: record.collectionId,
avatar: record.avatar,
phone: record.phone,
company: record.company,
timezone: record.timezone,
language: record.language,
currency: record.currency,
};
return temp;
}

View File

@@ -0,0 +1,10 @@
import { COL_USER_METAS } from '@/constants';
import type { RecordModel } from 'pocketbase';
import { pb } from '@/lib/pb';
import type { UpdateUserMeta } from './type';
export async function UpdateUserMetaById(id: string, data: Partial<UpdateUserMeta>): Promise<RecordModel> {
return pb.collection(COL_USER_METAS).update(id, data);
}

View File

@@ -0,0 +1,72 @@
// src/db/UserMetas/type.d.ts
//
// RULES: sorting direction for user meta lists
import type { BillingAddress } from '@/components/dashboard/user_meta/type.d';
export interface DBUserMeta {
name: string;
//
// NOTE: obslete "avatar" and use "avatar_file"
avatar?: string;
avatar_file?: string;
//
email: string;
phone: string;
quota: number;
company: string;
//
// billingAddress: BillingAddress[] | [];
expand: { billingAddress?: BillingAddress[] };
// status is obsoleted, replace by state
status: 'pending' | 'active' | 'blocked';
state: 'pending' | 'active' | 'blocked';
//
timezone: string;
language: string;
currency: string;
//
id: string;
created: string;
updated?: string;
collectionId: string;
}
// UserMeta type definitions
export interface UserMeta {
id: string;
name: string;
avatar: string;
email: string;
phone: string;
quota: number;
status: 'active' | 'blocked' | 'pending';
createdAt: Date;
}
export interface UpdateUserMeta {
name?: string;
//
// NOTE: obslete "avatar" and use "avatar_file"
// avatar_file?: string;
avatar: File | null;
//
email?: string;
phone?: string;
quota?: number;
company?: string;
//
// relation handle seperately
// billingAddress: BillingAddress | Record<string, never>;
// status is obsoleted, replace by state
// status: 'pending' | 'active' | 'blocked';
state?: 'pending' | 'active' | 'blocked';
//
timezone?: string;
language?: string;
currency?: string;
//
taxId?: string;
}

View File

@@ -1,15 +0,0 @@
// REQ0006
import { COL_USERS } from '@/constants';
import { pb } from '@/lib/pb';
export default async function GetAllCount(): Promise<number> {
try {
const result = await pb.collection(`users`).getList(1, 9999, { filter: 'email != ""' });
const { totalItems: count } = result;
return count;
} catch (error) {
console.error(error);
return -99;
}
}

View File

@@ -1,15 +1,9 @@
import { pb } from '@/lib/pb';
import { COL_USERS } from '@/constants';
import type { User } from '@/types/user';
export async function getUserById(id: string): Promise<User> {
try {
const user = await pb.collection(COL_USERS).getOne<User>(id);
return user;
} catch (err) {
if (err instanceof Error && err.message.includes('404')) {
throw new Error(`User with ID ${id} not found`);
}
throw err;
}
import { pb } from '@/lib/pb';
import type { User } from './type.d';
export function getUserById(id: string): Promise<User> {
return pb.collection(COL_USERS).getOne<User>(id);
}

View File

@@ -0,0 +1,10 @@
import { COL_USERS } from '@/constants';
import type { RecordModel } from 'pocketbase';
import { pb } from '@/lib/pb';
import type { UpdateUser } from './type.d';
export async function UpdateUserById(id: string, data: Partial<UpdateUser>): Promise<RecordModel> {
return pb.collection(COL_USERS).update(id, data);
}

View File

@@ -21,9 +21,12 @@ the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-onlin
simple template:
```typescript
import { pb } from '@/lib/pb';
import { COL_USERS } from '@/constants';
import { pb } from '@/lib/pb';
import type { User } from './type.d';
export async function createUser(data: CreateFormProps) {
// ...content
// use direct return of pb.collection (e.g. return pb.collection(xxx))

15
002_source/cms/src/db/Users/type.d.ts vendored Normal file
View File

@@ -0,0 +1,15 @@
//
// RULES
// pocketbase Users collection schema
//
// User type definitions
export interface User {
verified: boolean;
//
id: string;
createdAt: Date;
}
export interface UpdateUser {
verified?: boolean;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,36 @@
// src/db/billingAddress/GetById.tsx
//
// PURPOSE:
// to get billing address by its id
//
import { COL_BILLING_ADDRESS } from '@/constants';
import { pb } from '@/lib/pb';
import type { DBUserMeta, UserMeta } from '@/components/dashboard/user_meta/type.d';
export async function getBillingAddressById(id: string): Promise<UserMeta> {
const record = await pb
.collection(COL_BILLING_ADDRESS)
.getOne<DBUserMeta>(id, { expand: 'billingAddress, helloworld' });
const temp: UserMeta = {
id: record.id,
name: record.name,
email: record.email,
quota: record.quota,
billingAddress: record.expand.billingAddress ? record.expand.billingAddress[0] : {},
status: record.status,
state: record.state,
createdAt: new Date(record.created),
collectionId: record.collectionId,
avatar: record.avatar,
phone: record.phone,
company: record.company,
timezone: record.timezone,
language: record.language,
currency: record.currency,
};
return temp;
}

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
import { pb } from '@/lib/pb';
import { COL_CUSTOMERS } from '@/constants';
import type { RecordModel } from 'pocketbase';
import type { EditFormProps } from '@/components/dashboard/customer/type.d';
export async function updateCustomer(id: string, data: Partial<EditFormProps>): Promise<RecordModel> {
return pb.collection(COL_CUSTOMERS).update(id, data);
}

View File

@@ -0,0 +1,10 @@
import { COL_BILLING_ADDRESS } from '@/constants';
import type { RecordModel } from 'pocketbase';
import { pb } from '@/lib/pb';
import type { UpdateBillingAddress } from './type';
export async function UpdateBillingAddressById(id: string, data: Partial<UpdateBillingAddress>): Promise<RecordModel> {
return pb.collection(COL_BILLING_ADDRESS).update(id, data);
}

View File

@@ -0,0 +1,31 @@
# GUIDELINES
This folder contains drivers for `Customer`/`Customers` records using PocketBase:
- create (Create.tsx)
- read (GetById.tsx)
- write (Update.tsx)
- count (GetAllCount.tsx, GetActiveCount.tsx, GetBlockedCount.tsx, GetPendingCount.tsx)
- misc (Helloworld.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 `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Customers/type.d.tsx`
simple template:
```typescript
import { pb } from '@/lib/pb';
import { COL_CUSTOMERS } from '@/constants';
export async function createCustomer(data: CreateFormProps) {
// ...content
// use direct return of pb.collection (e.g. return pb.collection(xxx))
}
```

View File

@@ -0,0 +1,23 @@
export interface BillingAddress {
city: string;
country: string;
line1: string;
line2: string;
state: string;
zipCode: string;
//
id: string;
collectionId: string;
collectionName: string;
updated: string;
created: string;
}
export interface UpdateBillingAddress {
city?: string;
country?: string;
line1?: string;
line2?: string;
state?: string;
zipCode?: string;
}

View File

@@ -1,9 +1,12 @@
'use client';
// src/lib/auth/custom/client.ts
//
import { getUserMetaById } from '@/db/UserMetas/GetById';
import type { User } from '@/types/user';
import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
import type { User } from '@/types/user';
function generateToken(): string {
const arr = new Uint8Array(12);
@@ -11,14 +14,6 @@ function generateToken(): string {
return Array.from(arr, (v) => v.toString(16).padStart(2, '0')).join('');
}
const user_xxx = {
id: 'USR-000',
avatar: '/assets/avatar.png',
firstName: 'Sofia',
lastName: 'Rivers',
email: 'sofia@devias.io',
} satisfies User;
export interface SignUpParams {
firstName: string;
lastName: string;

View File

@@ -1,3 +1,8 @@
export default function getImageUrlFromFile(collectionId: string, id: string, catImage: string): string {
return `http://127.0.0.1:8090/api/files/${collectionId}/${id}/${catImage}`;
//
// PURPOSE:
// get file url from pocketbase record
//
export default function getImageUrlFromFile(collectionId: string, id: string, imgFile: string): string {
return `http://127.0.0.1:8090/api/files/${collectionId}/${id}/${imgFile}`;
}

View File

@@ -1,3 +1,7 @@
// src/lib/helloworld.ts
// RULES:
// T.B.A.
//
export function helloworld(): string {
return 'Helloworld';
}

View File

@@ -140,7 +140,7 @@ export const paths = {
list: '/dashboard/teachers/list',
create: '/dashboard/teachers/create',
details: (id: string) => `/dashboard/teachers/view/${id}`,
view: (id: string) => `/dashboard/students/view/${id}`,
view: (id: string) => `/dashboard/teachers/view/${id}`,
edit: (id: string) => `/dashboard/teachers/edit/${id}`,
mail: {
list: (id: string) => `/dashboard/teachers/mail/${id}/list`,

View File

@@ -1,149 +0,0 @@
volumes:
shared:
dist:
services:
cms:
image: 192.168.10.61:5000/cms_ubuntu
# build: ./cms
env_file:
- .env
volumes:
- ./cms:/app
ports:
- 3000:3000
working_dir: /app
command: ./scripts/docker/entrypoint.sh
depends_on:
pocketbase:
condition: service_healthy
healthcheck:
#optional (recommended) since v0.10.0
test: wget --no-verbose --tries=1 --spider http://localhost:3000 || exit 1
interval: 5s
timeout: 5s
retries: 5
deploy:
resources:
limits:
cpus: "0.5"
reservations:
cpus: "0.01"
doc:
build: ./doc
env_file:
- .env
volumes:
- ./doc:/app
ports:
- 3001:3000
working_dir: /app
command: ./scripts/docker/entrypoint.sh
healthcheck:
#optional (recommended) since v0.10.0
test: wget --no-verbose --tries=1 --spider http://localhost:3000 || exit 1
interval: 5s
timeout: 5s
retries: 5
deploy:
resources:
limits:
cpus: "0.5"
reservations:
cpus: "0.01"
ionic_mobile:
# image: node:20-bullseye-slim
# build: ./ionic_mobile
image: 192.168.10.61:5000/ionic_mobile_ubuntu
# user: 1000:1000
env_file:
- .env
volumes:
- ./ionic_mobile:/app
ports:
- 5173:5173
working_dir: /app
command: ./scripts/docker/entrypoint.sh
depends_on:
pocketbase:
condition: service_healthy
healthcheck:
#optional (recommended) since v0.10.0
test: wget --no-verbose --tries=1 --spider http://localhost:5173 || exit 1
interval: 5s
timeout: 5s
retries: 5
deploy:
resources:
limits:
cpus: "0.5"
reservations:
cpus: "0.01"
api_ts:
image: 192.168.10.61:5000/api_ts_ubuntu
# build: ./api_ts
volumes:
- ./api_ts:/app
working_dir: /app
# env_file:
# - .env
environment:
- NODE_ENV=production
- PB_HOSTNAME=pocketbase
- PB_USERNAME=admin@123.com
- PB_PASSWORD=Aa12345678
ports:
- 8080:3000
# command: sleep infinity
command: ./entrypoint.sh
# depends_on:
# pocketbase:
# condition: service_healthy
# healthcheck:
# #optional (recommended) since v0.10.0
# test: wget --no-verbose --tries=1 --spider http://localhost:3000 || exit 1
# interval: 5s
# timeout: 5s
# retries: 5
deploy:
resources:
limits:
cpus: 0.5
reservations:
cpus: 0.01
pocketbase:
# image: ghcr.io/muchobien/pocketbase:latest
build:
context: ./pocketbase/docker
args:
- VERSION=0.26.6 # Specify the PocketBase version here
# hostname: pocketbase
restart: always
# environment:
# ENCRYPTION: example #optional
ports:
- 8090:8090
volumes:
- ./pocketbase/volumes/pb_data:/pb_data
- ./pocketbase/pb_hooks:/pb_hooks
- ./pocketbase/pb_migrations:/pb_migrations
# healthcheck:
# #optional (recommended) since v0.10.0
# test: wget --no-verbose --tries=1 --spider http://localhost:8090/api/health || exit 1
# interval: 5s
# timeout: 5s
# retries: 5
deploy:
resources:
limits:
cpus: 0.5
reservations:
cpus: 0.01

View File

@@ -1,3 +1,4 @@
# TODO
- [ ] add login mechanism
- [ ] add task server handle callback tasks

View File

@@ -16,6 +16,7 @@
"@capacitor/keyboard": "6.0.3",
"@capacitor/splash-screen": "^6.0.3",
"@capacitor/status-bar": "6.0.2",
"@hookform/resolvers": "3.3.4",
"@ionic/prettier-config": "^4.0.0",
"@ionic/react": "^8.0.0",
"@ionic/react-router": "^8.0.0",
@@ -23,21 +24,25 @@
"@lifeomic/attempt": "^3.1.0",
"@tanstack/react-query": "^5.74.4",
"@tanstack/react-query-devtools": "^5.74.6",
"@types/lodash": "^4.17.16",
"@types/react-router": "^5.1.20",
"@types/react-router-dom": "^5.3.3",
"axios": "^1.8.1",
"i18next": "^24.2.2",
"ionicons": "^7.0.0",
"lodash": "^4.17.21",
"pocketbase": "^0.26.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "7.50.1",
"react-i18next": "^15.4.1",
"react-markdown": "^9.0.3",
"react-router": "^5.3.4",
"react-router-dom": "^5.3.4",
"react-use": "^17.6.0",
"react-use-audio-player": "^2.3.0-alpha.1",
"remark-gfm": "^4.0.0"
"remark-gfm": "^4.0.0",
"zod": "3.22.4"
},
"devDependencies": {
"@capacitor/assets": "^3.0.5",
@@ -65,7 +70,7 @@
"vitest": "^0.34.6"
},
"engines": {
"node": "==18"
"node": "==22"
}
},
"node_modules/@adobe/css-tools": {
@@ -2242,6 +2247,15 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@hookform/resolvers": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.4.tgz",
"integrity": "sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==",
"license": "MIT",
"peerDependencies": {
"react-hook-form": "^7.0.0"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@@ -3699,6 +3713,12 @@
"integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==",
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.16",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz",
"integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==",
"license": "MIT"
},
"node_modules/@types/mdast": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
@@ -10602,7 +10622,6 @@
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.debounce": {
@@ -13138,6 +13157,22 @@
"react": "^18.3.1"
}
},
"node_modules/react-hook-form": {
"version": "7.50.1",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.50.1.tgz",
"integrity": "sha512-3PCY82oE0WgeOgUtIr3nYNNtNvqtJ7BZjsbxh6TnYNbXButaD5WpjOmTjdxZfheuHKR68qfeFnEDVYoSSFPMTQ==",
"license": "MIT",
"engines": {
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/react-hook-form"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18"
}
},
"node_modules/react-i18next": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.1.tgz",
@@ -16747,6 +16782,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zod": {
"version": "3.22.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
"integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",

View File

@@ -31,6 +31,7 @@
"@capacitor/keyboard": "6.0.3",
"@capacitor/splash-screen": "^6.0.3",
"@capacitor/status-bar": "6.0.2",
"@hookform/resolvers": "3.3.4",
"@ionic/prettier-config": "^4.0.0",
"@ionic/react": "^8.0.0",
"@ionic/react-router": "^8.0.0",
@@ -38,21 +39,25 @@
"@lifeomic/attempt": "^3.1.0",
"@tanstack/react-query": "^5.74.4",
"@tanstack/react-query-devtools": "^5.74.6",
"@types/lodash": "^4.17.16",
"@types/react-router": "^5.1.20",
"@types/react-router-dom": "^5.3.3",
"axios": "^1.8.1",
"i18next": "^24.2.2",
"ionicons": "^7.0.0",
"lodash": "^4.17.21",
"pocketbase": "^0.26.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "7.50.1",
"react-i18next": "^15.4.1",
"react-markdown": "^9.0.3",
"react-router": "^5.3.4",
"react-router-dom": "^5.3.4",
"react-use": "^17.6.0",
"react-use-audio-player": "^2.3.0-alpha.1",
"remark-gfm": "^4.0.0"
"remark-gfm": "^4.0.0",
"zod": "3.22.4"
},
"devDependencies": {
"@capacitor/assets": "^3.0.5",
@@ -81,6 +86,6 @@
},
"description": "An Ionic project",
"engines": {
"node": "==18"
"node": "==22"
}
}

View File

@@ -0,0 +1,16 @@
const Paths = {
AuthHome: `/auth/home`,
AuthLogin: `/auth/login`,
AuthSignUp: `/auth/signup`,
SignUpSuccess: `/auth/sign_up_success`,
//
StudentMenu: `/auth/student_menu`,
StudentInfo: `/auth/student_info/:id`,
GetStudentInfoLink: (id: string) => `/auth/student_info/${id}`,
//
AuthorizedTest: `/auth/authorized_test`,
//
Setting: `/setting`,
};
export { Paths };

View File

@@ -45,10 +45,17 @@ import Page from './pages/Page';
import QuizzesMainMenu from './pages/QuizzesMainMenu';
//
import MyAchievementPage from './pages/Record/index';
import Setting from './pages/Setting/indx';
import Setting from './pages/Setting';
import Tab1 from './pages/Tab1';
import Tab2 from './pages/Tab2';
import Tab3 from './pages/Tab3';
import { Paths } from './Paths';
import SignUpSuccess from './pages/auth/SignUpSuccess';
import AuthorizedTest from './pages/auth/AuthorizedTest';
import { AuthGuard } from './components/auth/auth-guard';
import StudentInfo from './pages/auth/StudentInfo';
import StudentMenu from './pages/auth/StudentMenu';
// import { AuthGuard } from './pages/auth/AuthorizedTest/auth-guard';
// import WordPageWithLayout from './pages/Lesson/WordPageWithLayout.del';
function RouteConfig() {
@@ -160,18 +167,35 @@ function RouteConfig() {
<ConnectivesPage />
</Route>
<Route exact path={`/auth/Home`}>
<Route exact path={Paths.AuthHome}>
<AuthHome />
</Route>
<Route exact path={`/auth/login`}>
<Route exact path={Paths.AuthLogin}>
<AuthLogin />
</Route>
<Route exact path={`/auth/signup`}>
<Route exact path={Paths.AuthSignUp}>
<AuthSignUp />
</Route>
<Route exact path={Paths.SignUpSuccess}>
<SignUpSuccess />
</Route>
{/* protected page */}
<AuthGuard>
<Route exact path={Paths.StudentInfo}>
<StudentInfo />
</Route>
<Route exact path={Paths.StudentMenu}>
<StudentMenu />
</Route>
<Route exact path={Paths.AuthorizedTest}>
<AuthorizedTest />
</Route>
</AuthGuard>
{/* TODO: remove below */}
<Route exact path="/tab1">
<Tab1 />
@@ -182,7 +206,7 @@ function RouteConfig() {
<Route path="/tab3">
<Tab3 />
</Route>
<Route path="/setting">
<Route path={Paths.Setting}>
<Setting />
</Route>
<Route path="/page/:name" exact={true}>

View File

@@ -1,10 +1,7 @@
import { IonInput, IonLabel } from '@ionic/react';
import styles from './style.module.scss';
function CustomField({
field,
errors,
}: {
interface CustomFieldProps {
field: {
id: string;
label: string;
@@ -20,7 +17,9 @@ function CustomField({
};
};
errors: any;
}): React.JSX.Element {
}
function CustomField({ field, errors }: CustomFieldProps): React.JSX.Element {
const error = errors && errors.filter((e) => e.id === field.id)[0];
const errorMessage = error && errors.filter((e) => e.id === field.id)[0].message;

View File

@@ -2,6 +2,8 @@ import { IonButton, IonIcon, useIonRouter } from '@ionic/react';
import { arrowBack } from 'ionicons/icons';
import { LESSON_LINK, VERSIONS } from '../../constants';
import SettingSvg from './image.svg';
import { Paths } from '../../Paths';
import { pb } from '../../lib/pb';
interface ContainerProps {
name: string;
@@ -9,6 +11,19 @@ interface ContainerProps {
const SettingContainer: React.FC<ContainerProps> = ({ name }) => {
const router = useIonRouter();
function handleAuthHomeClick() {
router.push(Paths.AuthHome);
}
function handleUserProfileClick() {
if (pb.authStore.record?.id) {
router.push(Paths.GetStudentInfoLink(pb.authStore.record.id));
} else {
router.push(Paths.AuthLogin);
}
}
return (
<div
style={{
@@ -34,6 +49,7 @@ const SettingContainer: React.FC<ContainerProps> = ({ name }) => {
<p>T.B.A.</p>
</div>
<div>{VERSIONS}</div>
<IonButton onClick={handleUserProfileClick}>User Profile</IonButton>
<IonButton
onClick={() => {
router.push(LESSON_LINK, undefined, 'replace');

View File

@@ -0,0 +1,52 @@
import { useIonRouter } from '@ionic/react';
import * as React from 'react';
import { IonAlert, IonButton } from '@ionic/react';
import { useUser } from '../../hooks/use-user';
import { Paths } from '../../Paths';
export interface AuthGuardProps {
children: React.ReactNode;
}
export function AuthGuard({ children }: AuthGuardProps): React.JSX.Element | null {
const router = useIonRouter();
const { user, error, isLoading } = useUser();
const [isChecking, setIsChecking] = React.useState<boolean>(true);
const checkPermissions = async (): Promise<void> => {
//
if (isLoading) {
return;
}
//
if (error) {
setIsChecking(false);
return;
}
// NOTE: here state that if user = null, eject user to login page
if (!user) {
// logger.debug('[AuthGuard]: User is not logged in, redirecting to sign in');
router.push(Paths.AuthLogin);
}
setIsChecking(false);
};
React.useEffect(() => {
checkPermissions().catch(() => {
// noop
});
// eslint-disable-next-line react-hooks/exhaustive-deps -- Expected
}, [user, error, isLoading]);
if (isChecking) {
return null;
}
if (error) {
return <IonAlert color="error">{error}</IonAlert>;
}
return <React.Fragment>{children}</React.Fragment>;
}

View File

@@ -77,6 +77,10 @@ const MY_FAVORITE = 'My Favorite';
//
const POCKETBASE_URL = import.meta.env.VITE_POCKETBASE_URL;
//
// database constants
export const COL_USERS = 'users';
export const COL_USER_METAS = 'UserMetas';
export {
//

View File

@@ -0,0 +1,54 @@
import * as React from 'react';
import { User } from '../../../types/user';
import { authClient } from '../../../lib/auth/custom/client';
import type { UserContextValue } from '../types';
export const UserContext = React.createContext<UserContextValue | undefined>(undefined);
export interface UserProviderProps {
children: React.ReactNode;
}
export function UserProvider({ children }: UserProviderProps): React.JSX.Element {
const [state, setState] = React.useState<{ user: User | null; error: string | null; isLoading: boolean }>({
user: null,
error: null,
isLoading: true,
});
const checkSession = React.useCallback(async (): Promise<void> => {
try {
const { data, error } = await authClient.getUser();
if (error) {
// logger.error(error);
setState((prev) => ({ ...prev, user: null, error: 'Something went wrong', isLoading: false }));
return;
}
setState((prev) => ({ ...prev, user: data ?? null, error: null, isLoading: false }));
} catch (err) {
// logger.error(err);
setState((prev) => ({ ...prev, user: null, error: 'Something went wrong', isLoading: false }));
}
}, []);
React.useEffect(() => {
checkSession().catch((err) => {
// logger.error(err);
// noop
});
// eslint-disable-next-line react-hooks/exhaustive-deps -- Expected
}, []);
return (
<UserContext.Provider value={{ ...state, checkSession }}>
{/* */}
{children}
</UserContext.Provider>
);
}
export const UserConsumer = UserContext.Consumer;

View File

@@ -0,0 +1,8 @@
import type { User } from '../../types/user';
export interface UserContextValue {
user: User | null;
error: string | null;
isLoading: boolean;
checkSession?: () => Promise<void>;
}

View File

@@ -0,0 +1,16 @@
import type * as React from 'react';
import type { UserContextValue } from './types';
import { UserContext as CustomUserContext, UserProvider as CustomUserProvider } from './custom/user-context';
// eslint-disable-next-line import/no-mutable-exports -- Export based on config
let UserProvider: React.FC<{ children: React.ReactNode }>;
// eslint-disable-next-line import/no-mutable-exports -- Export based on config
let UserContext: React.Context<UserContextValue | undefined>;
UserContext = CustomUserContext;
UserProvider = CustomUserProvider;
export { UserProvider, UserContext };

View File

@@ -1,5 +1,6 @@
import { PocketBaseProvider } from '../hooks/usePocketBase';
import { AppStateProvider } from './AppState';
import { UserProvider } from './auth/user-context';
import { MyIonFavoriteProvider } from './MyIonFavorite';
import { MyIonMetricProvider } from './MyIonMetric';
import { MyIonQuizProvider } from './MyIonQuiz';
@@ -12,6 +13,7 @@ const ContextMeta = ({ children }: { children: React.ReactNode }) => {
return (
<>
<AppStateProvider>
<UserProvider>
<MyIonStoreProvider>
<MyIonFavoriteProvider>
<MyIonQuizProvider>
@@ -26,6 +28,7 @@ const ContextMeta = ({ children }: { children: React.ReactNode }) => {
</MyIonQuizProvider>
</MyIonFavoriteProvider>
</MyIonStoreProvider>
</UserProvider>
</AppStateProvider>
</>
);

View File

@@ -7,10 +7,7 @@ export const useSignupFields = () => {
label: 'Name',
required: true,
input: {
props: {
type: 'text',
placeholder: 'Joe Bloggs',
},
props: { type: 'text', placeholder: 'Joe Bloggs' },
state: useFormInput(''),
},
},
@@ -19,10 +16,7 @@ export const useSignupFields = () => {
label: 'Email',
required: true,
input: {
props: {
type: 'email',
placeholder: 'joe@bloggs.com',
},
props: { type: 'email', placeholder: 'joe@bloggs.com' },
state: useFormInput(''),
},
},
@@ -31,10 +25,7 @@ export const useSignupFields = () => {
label: 'Password',
required: true,
input: {
props: {
type: 'password',
placeholder: '*********',
},
props: { type: 'password', placeholder: '*********' },
state: useFormInput(''),
},
},

View File

@@ -0,0 +1,11 @@
import type { RecordModel } from 'pocketbase';
import { COL_USER_METAS } from '../../constants';
import { pb } from '../../lib/pb';
export async function getUserMetaById(id: string): Promise<RecordModel> {
return pb.collection(COL_USER_METAS).getOne(id, {
expand: 'billingAddress',
requestKey: null,
});
}

View File

@@ -1,11 +1,12 @@
# GUIDELINES
This folder contains drivers for `User`/`Users` records using PocketBase:
This folder contains drivers for `UserMeta`/`UserMetas`(Collection ID: pbc_1305841361) records using PocketBase:
- create (Create.tsx)
- read (GetById.tsx)
- write (Update.tsx)
- count (GetAllCount.tsx)
- count (GetAllCount.tsx, GetActiveCount.tsx, GetBlockedCount.tsx, GetPendingCount.tsx)
- misc (Helloworld.tsx)
- delete (Delete.tsx)
- list (GetAll.tsx)
@@ -15,15 +16,15 @@ the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-onlin
- 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/Users/type.d.tsx`
- type information defined in `./type.d.tsx`
simple template:
```typescript
import { pb } from '@/lib/pb';
import { COL_USERS } from '@/constants';
import { COL_CUSTOMERS } from '@/constants';
export async function createUser(data: CreateFormProps) {
export async function createCustomer(data: CreateFormProps) {
// ...content
// use direct return of pb.collection (e.g. return pb.collection(xxx))
}

View File

@@ -0,0 +1,11 @@
`working directory`: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/UserMetas`
these files are clone from elsewhere,
please help to list `*.tsx.draft` files in `working directory` (e.g. `find`),
iterate the files listed in the result.
please understand, modify and update the content to handle `UserMeta` record thanks, modify comments/variables/paths/functions name please
restrict your modifications in working directory only,
I will handle all the modification outside this direcotry
e.g. if `lessonCategories` exist in file, modify it to `userMetas`

View File

@@ -0,0 +1,53 @@
import type { BillingAddress } from '@/components/dashboard/user_meta/type.d';
// DBUserMeta type definitions
export interface DBUserMeta {
name: string;
avatar: string;
email: string;
phone: string;
quota: number;
status: 'active' | 'blocked' | 'pending';
//
collectionId: string;
id: string;
createdAt: Date;
}
// UserMeta type definitions
export interface UserMeta {
id: string;
name: string;
avatar: string;
email: string;
phone: string;
quota: number;
status: 'active' | 'blocked' | 'pending';
createdAt: Date;
}
export interface UpdateUserMeta {
name?: string;
//
// NOTE: obslete "avatar" and use "avatar_file"
// avatar_file?: string;
avatar: File | null;
//
email?: string;
phone?: string;
quota?: number;
company?: string;
//
// relation handle seperately
// billingAddress: BillingAddress | Record<string, never>;
// status is obsoleted, replace by state
// status: 'pending' | 'active' | 'blocked';
state?: 'pending' | 'active' | 'blocked';
//
timezone?: string;
language?: string;
currency?: string;
//
taxId?: string;
}

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