diff --git a/.gitignore b/.gitignore index 6d83ae8..05d9498 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ _del *.log *.del **/*del +**/*old **/volumes/** 006_lab diff --git a/000_AI_WORKSPACE/software-engineer/greetings/002_guideline.md b/000_AI_WORKSPACE/software-engineer/greetings/002_guideline.md index f6decdf..5ea4c6f 100644 --- a/000_AI_WORKSPACE/software-engineer/greetings/002_guideline.md +++ b/000_AI_WORKSPACE/software-engineer/greetings/002_guideline.md @@ -31,3 +31,7 @@ - `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. diff --git a/001_documentation/Requirements/REQ0016/index.md b/001_documentation/Requirements/REQ0016/index.md index f3de00a..a60f8e3 100644 --- a/001_documentation/Requirements/REQ0016/index.md +++ b/001_documentation/Requirements/REQ0016/index.md @@ -2,7 +2,7 @@ tags: cms, login-flow --- -# login flow +# CMS login flow ## description @@ -31,3 +31,9 @@ graph TD; Start((start)); End((end)) ``` + +## test + +![alt text](test.gif) + +## relation diff --git a/001_documentation/Requirements/REQ0016/test.gif b/001_documentation/Requirements/REQ0016/test.gif new file mode 100644 index 0000000..1fb2813 Binary files /dev/null and b/001_documentation/Requirements/REQ0016/test.gif differ diff --git a/001_documentation/Requirements/REQ0020/index.md b/001_documentation/Requirements/REQ0020/index.md new file mode 100644 index 0000000..15ef629 --- /dev/null +++ b/001_documentation/Requirements/REQ0020/index.md @@ -0,0 +1,41 @@ +--- +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)) +``` + +## test + +![alt text](test.gif) + +### relations + +[REQ0016](../REQ0016/index.md) diff --git a/001_documentation/Requirements/REQ0020/test.gif b/001_documentation/Requirements/REQ0020/test.gif new file mode 100644 index 0000000..b1beddc Binary files /dev/null and b/001_documentation/Requirements/REQ0020/test.gif differ diff --git a/001_documentation/Requirements/index.md b/001_documentation/Requirements/index.md index 526b7a8..5431949 100644 --- a/001_documentation/Requirements/index.md +++ b/001_documentation/Requirements/index.md @@ -17,7 +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) diff --git a/001_documentation/documentation.code-workspace b/001_documentation/documentation.code-workspace new file mode 100644 index 0000000..e3aad38 --- /dev/null +++ b/001_documentation/documentation.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "../003_test" + } + ], + "settings": {} +} diff --git a/001_documentation/o7f74lN9wZ.mp4 b/001_documentation/o7f74lN9wZ.mp4 new file mode 100644 index 0000000..4553d08 Binary files /dev/null and b/001_documentation/o7f74lN9wZ.mp4 differ diff --git a/002_source/cms/.env.development b/002_source/cms/.env.development index c142266..bb906c4 100644 --- a/002_source/cms/.env.development +++ b/002_source/cms/.env.development @@ -58,4 +58,4 @@ NEXT_PUBLIC_MAPBOX_API_KEY= # Google Tag Manager NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID= -NEXT_PUBLIC_POCKETBASE_URL=http://localhost:8090 +NEXT_PUBLIC_POCKETBASE_URL=http://192.168.222.199:8090 diff --git a/002_source/cms/.eslintrc.js b/002_source/cms/.eslintrc.js index 9df006e..3660e61 100644 --- a/002_source/cms/.eslintrc.js +++ b/002_source/cms/.eslintrc.js @@ -82,7 +82,13 @@ module.exports = { 'eslintreact/jsx-sort-props': 'off', 'react/jsx-sort-props': 'off', }, - ignorePatterns: ['**/*del', '**/*bak', '**/*copy.*', '**/*copy*.*'], + ignorePatterns: [ + '**/*.del', + '**/*.bak', + '**/*copy.*', + '**/*copy*.*', + // + ], overrides: [ { // override to ignore no-def for `describe`, `it`, and `expect` diff --git a/002_source/cms/default.code-workspace b/002_source/cms/cms.code-workspace similarity index 100% rename from 002_source/cms/default.code-workspace rename to 002_source/cms/cms.code-workspace diff --git a/002_source/cms/package.json b/002_source/cms/package.json index 8f01c52..47bd08c 100644 --- a/002_source/cms/package.json +++ b/002_source/cms/package.json @@ -7,7 +7,7 @@ "node": "==22" }, "scripts": { - "dev": "next dev", + "dev": "next dev -H 0.0.0.0", "build": "next build", "build:w": "pnpx nodemon --ext ts,tsx,json,mjs,js,jsx --delay 15 --exec \"pnpm run build\"", "start": "next start", diff --git a/002_source/cms/prettier.config.mjs b/002_source/cms/prettier.config.mjs index 39fc458..2331ec3 100644 --- a/002_source/cms/prettier.config.mjs +++ b/002_source/cms/prettier.config.mjs @@ -28,7 +28,10 @@ const config = { '', '^[./]', ], - plugins: ['@ianvs/prettier-plugin-sort-imports'], + plugins: [ + '@ianvs/prettier-plugin-sort-imports', + // + ], overrides: [ { files: ['*.tsx'], diff --git a/002_source/cms/public/assets/logo-github.svg b/002_source/cms/public/assets/logo-github.svg new file mode 100644 index 0000000..c3c6961 --- /dev/null +++ b/002_source/cms/public/assets/logo-github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/002_source/cms/public/locales/dev/sign_in.json b/002_source/cms/public/locales/dev/sign_in.json new file mode 100644 index 0000000..158b40a --- /dev/null +++ b/002_source/cms/public/locales/dev/sign_in.json @@ -0,0 +1,31 @@ +{ + "sign-in": "Sign in / 登入", + "dont-have-an-account": "Don't have an account ?", + "continue-with_fh": "以", + "continue-with_sh": "繼續", + "forgot-password": "Forgot password", + "user": "用戶名稱", + "password": "密碼", + "email-address": "用戶電郵", + "first-name-is-required": "名字是必填项", + "last-name-is-required": "姓氏不能为空", + "email-is-required": "邮箱不能为空", + "password-should-be-at-least-6-characters": "密码至少需要6个字符", + "you-must-accept-the-terms-and-conditions": "您必须接受条款和条件", + "sign-up": "注册", + "already-have-an-account": "已有账号?", + "or": "或", + "create-account": "创建账号", + "created-users-are-not-persisted": "已创建的用户不会被保存", + "i-have-read-the": "我已阅读", + "terms-and-conditions": "用户协议和隐私政策", + "e.g.": "例如:", + "first-name": "名", + "last-name": "姓", + "hello": "world", + "welcome-title": "Welcome to devias kit pro", + "welcome-notes": "A professional template that comes with ready-to-use MUI components developed with one common goal in mind, help you build faster & beautiful applications.", + "reset-password": "重置密码", + "back-to-login": "返回登录", + "send-recovery-link": "发送恢复链接" +} diff --git a/002_source/cms/src/__tests__/app/_helloworld/page.test.tsx b/002_source/cms/src/__tests__/app/_helloworld/page.test.tsx deleted file mode 100644 index 5c198ca..0000000 --- a/002_source/cms/src/__tests__/app/_helloworld/page.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/// -import '@testing-library/jest-dom'; -import { render, screen } from '@testing-library/react'; -import Page from '@/app/_helloworld/page'; - -// Mock the translation hook -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})); - -describe('Page', () => { - it('renders a heading', () => { - render(); - - const heading = screen.getByRole('heading', { level: 1 }); - - expect(heading).toBeInTheDocument(); - }); -}); diff --git a/002_source/cms/src/__tests__/snapshot.js b/002_source/cms/src/__tests__/snapshot.js deleted file mode 100644 index c070dc2..0000000 --- a/002_source/cms/src/__tests__/snapshot.js +++ /dev/null @@ -1,9 +0,0 @@ -import { render } from '@testing-library/react'; - -// CUT = Component Under Test -import CUT from '../components/_helloworld'; - -it('renders homepage unchanged', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); -}); diff --git a/002_source/cms/src/app/dashboard/connectives/lesson-categories-sample-data.tsx b/002_source/cms/src/app/dashboard/connectives/lesson-categories-sample-data.tsx index 6a815ad..3d72108 100644 --- a/002_source/cms/src/app/dashboard/connectives/lesson-categories-sample-data.tsx +++ b/002_source/cms/src/app/dashboard/connectives/lesson-categories-sample-data.tsx @@ -1,5 +1,5 @@ import { dayjs } from '@/lib/dayjs'; -import { LessonCategory } from '@/components/dashboard/lesson_category/type'; +import type { LessonCategory } from '@/components/dashboard/lesson_category/type'; // import type { LessonCategory } from '@/components/dashboard/lp_categories/type'; diff --git a/002_source/cms/src/app/dashboard/connectives/view/[cat_id]/page.tsx b/002_source/cms/src/app/dashboard/connectives/view/[cat_id]/page.tsx index cc14c52..25c80a9 100644 --- a/002_source/cms/src/app/dashboard/connectives/view/[cat_id]/page.tsx +++ b/002_source/cms/src/app/dashboard/connectives/view/[cat_id]/page.tsx @@ -28,12 +28,13 @@ import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning'; import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; -import { RecordModel } from 'pocketbase'; +import type { RecordModel } from 'pocketbase'; import { useTranslation } from 'react-i18next'; import { paths } from '@/paths'; import { dayjs } from '@/lib/dayjs'; import { logger } from '@/lib/default-logger'; +import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts'; import { pb } from '@/lib/pb'; import { PropertyItem } from '@/components/core/property-item'; import { PropertyList } from '@/components/core/property-list'; @@ -45,7 +46,7 @@ import { Notifications } from '@/components/dashboard/lesson_category/notificati import { Payments } from '@/components/dashboard/lesson_category/payments'; import type { Address } from '@/components/dashboard/lesson_category/shipping-address'; import { ShippingAddress } from '@/components/dashboard/lesson_category/shipping-address'; -import { LessonCategory } from '@/components/dashboard/lesson_category/type'; +import type { LessonCategory } from '@/components/dashboard/lesson_category/type'; // import type { LessonCategory } from '@/components/dashboard/lp_categories/type'; import FormLoading from '@/components/loading'; @@ -119,39 +120,72 @@ export default function Page(): React.JSX.Element { {t('dashboard.lessonCategorys.list.title')} - - + + empty
- + {showLessonCategory.name} } + icon={ + + } label={showLessonCategory.visible} size="small" variant="outlined" /> - + {showLessonCategory.id}
-
- - + + {( [ - { key: 'Customer ID', value: }, + { + key: 'Customer ID', + value: ( + + ), + }, { key: 'Name', value: showLessonCategory.name }, { key: 'Pos', value: showLessonCategory.pos }, { @@ -195,9 +238,20 @@ export default function Page(): React.JSX.Element { { key: 'Quota', value: ( - - - + + + 50% @@ -206,7 +260,11 @@ export default function Page(): React.JSX.Element { ] satisfies { key: string; value: React.ReactNode }[] ).map( (item): React.JSX.Element => ( - + ) )} @@ -223,11 +281,17 @@ export default function Page(): React.JSX.Element {
-
- + A deleted lesson category cannot be restored. All data will be permanently removed.
@@ -235,7 +299,10 @@ export default function Page(): React.JSX.Element {
- + }> + } @@ -294,8 +364,14 @@ export default function Page(): React.JSX.Element { title={t('billing-details', { ns: 'lesson_category' })} /> - - } sx={{ '--PropertyItem-padding': '16px' }}> + + } + sx={{ '--PropertyItem-padding': '16px' }} + > {( [ { key: 'Credit card', value: '**** 4142' }, @@ -307,7 +383,11 @@ export default function Page(): React.JSX.Element { ] satisfies { key: string; value: React.ReactNode }[] ).map( (item): React.JSX.Element => ( - + ) )} @@ -317,7 +397,10 @@ export default function Page(): React.JSX.Element { }> + } @@ -329,7 +412,10 @@ export default function Page(): React.JSX.Element { title={t('shipping-addresses', { ns: 'lesson_category' })} /> - + {( [ { @@ -351,7 +437,11 @@ export default function Page(): React.JSX.Element { }, ] satisfies Address[] ).map((address) => ( - + ))} diff --git a/002_source/cms/src/app/dashboard/cr/categories/list/page.tsx b/002_source/cms/src/app/dashboard/cr/categories/list/page.tsx index c00990e..b3fcdbb 100644 --- a/002_source/cms/src/app/dashboard/cr/categories/list/page.tsx +++ b/002_source/cms/src/app/dashboard/cr/categories/list/page.tsx @@ -90,8 +90,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { React.useEffect(() => { if (!isFirstRun.current) { isFirstRun.current = true; - } else { - if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) { + } else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) { // reset page number as tab changes setLastListOption(listOption); setCurrentPage(0); @@ -99,7 +98,6 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { } else { void reloadRows(); } - } }, [currentPage, rowsPerPage, listOption]); React.useEffect(() => { diff --git a/002_source/cms/src/app/dashboard/cr/categories/lp-categories-sample-data.tsx b/002_source/cms/src/app/dashboard/cr/categories/lp-categories-sample-data.tsx index 0c1aa79..8054df7 100644 --- a/002_source/cms/src/app/dashboard/cr/categories/lp-categories-sample-data.tsx +++ b/002_source/cms/src/app/dashboard/cr/categories/lp-categories-sample-data.tsx @@ -1,5 +1,5 @@ import { dayjs } from '@/lib/dayjs'; -import { LessonCategory } from '@/components/dashboard/lesson_category/type'; +import type { LessonCategory } from '@/components/dashboard/lesson_category/type'; export const LpCategoriesSampleData = [ { diff --git a/002_source/cms/src/app/dashboard/cr/categories/view/[cat_id]/BasicDetailCard.tsx b/002_source/cms/src/app/dashboard/cr/categories/view/[cat_id]/BasicDetailCard.tsx index e79c927..8012466 100644 --- a/002_source/cms/src/app/dashboard/cr/categories/view/[cat_id]/BasicDetailCard.tsx +++ b/002_source/cms/src/app/dashboard/cr/categories/view/[cat_id]/BasicDetailCard.tsx @@ -13,7 +13,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 { CrCategory } from '@/components/dashboard/cr/categories/type'; export default function BasicDetailCard({ lpModel: model, diff --git a/002_source/cms/src/app/dashboard/cr/categories/view/[cat_id]/TitleCard.tsx b/002_source/cms/src/app/dashboard/cr/categories/view/[cat_id]/TitleCard.tsx index 9994060..37b6dea 100644 --- a/002_source/cms/src/app/dashboard/cr/categories/view/[cat_id]/TitleCard.tsx +++ b/002_source/cms/src/app/dashboard/cr/categories/view/[cat_id]/TitleCard.tsx @@ -10,12 +10,9 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; import { useTranslation } from 'react-i18next'; +import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts'; import type { CrCategory } from '@/components/dashboard/cr/categories/type'; -function getImageUrlFrRecord(record: CrCategory): string { - return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; -} - export default function SampleTitleCard({ lpModel }: { lpModel: CrCategory }): React.JSX.Element { const { t } = useTranslation(); @@ -28,7 +25,7 @@ export default function SampleTitleCard({ lpModel }: { lpModel: CrCategory }): R > {t('empty')} diff --git a/002_source/cms/src/app/dashboard/cr/questions/[cat_id]/BasicDetailCard.tsx b/002_source/cms/src/app/dashboard/cr/questions/[cat_id]/BasicDetailCard.tsx index 6683056..3a1a777 100644 --- a/002_source/cms/src/app/dashboard/cr/questions/[cat_id]/BasicDetailCard.tsx +++ b/002_source/cms/src/app/dashboard/cr/questions/[cat_id]/BasicDetailCard.tsx @@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next'; import { PropertyItem } from '@/components/core/property-item'; import { PropertyList } from '@/components/core/property-list'; -import { LpCategory } from '@/components/dashboard/lp/categories/type'; +import type { LpCategory } from '@/components/dashboard/lp/categories/type'; export default function BasicDetailCard({ lpModel: model, diff --git a/002_source/cms/src/app/dashboard/cr/questions/[cat_id]/TitleCard.tsx b/002_source/cms/src/app/dashboard/cr/questions/[cat_id]/TitleCard.tsx index 6e38fb6..5dfa618 100644 --- a/002_source/cms/src/app/dashboard/cr/questions/[cat_id]/TitleCard.tsx +++ b/002_source/cms/src/app/dashboard/cr/questions/[cat_id]/TitleCard.tsx @@ -10,11 +10,8 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; import { useTranslation } from 'react-i18next'; -import { LpCategory } from '@/components/dashboard/lp/categories/type'; - -function getImageUrlFrRecord(record: LpCategory): string { - return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; -} +import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts'; +import type { LpCategory } from '@/components/dashboard/lp/categories/type'; export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): React.JSX.Element { const { t } = useTranslation(); @@ -28,7 +25,7 @@ export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): R > {t('empty')} diff --git a/002_source/cms/src/app/dashboard/cr/questions/cr-categories-sample-data.tsx b/002_source/cms/src/app/dashboard/cr/questions/cr-categories-sample-data.tsx index 6a9b57e..c85443e 100644 --- a/002_source/cms/src/app/dashboard/cr/questions/cr-categories-sample-data.tsx +++ b/002_source/cms/src/app/dashboard/cr/questions/cr-categories-sample-data.tsx @@ -1,5 +1,5 @@ import { dayjs } from '@/lib/dayjs'; -import { LessonCategory } from '@/components/dashboard/lesson_category/type'; +import type { LessonCategory } from '@/components/dashboard/lesson_category/type'; export const CrCategoriesSampleData = [ { diff --git a/002_source/cms/src/app/dashboard/customers/[customerId]/TitleCard.tsx b/002_source/cms/src/app/dashboard/customers/[customerId]/TitleCard.tsx index 971bfa7..4c7bf8e 100644 --- a/002_source/cms/src/app/dashboard/customers/[customerId]/TitleCard.tsx +++ b/002_source/cms/src/app/dashboard/customers/[customerId]/TitleCard.tsx @@ -9,14 +9,14 @@ import Typography from '@mui/material/Typography'; import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; import { useTranslation } from 'react-i18next'; + import type { Customer } from '@/components/dashboard/customer/type.d'; // import type { CrCategory } from '@/components/dashboard/cr/categories/type'; function getImageUrlFrRecord(record: Customer): string { - // TODO: fix this - // `http://127.0.0.1:8090/api/files/${'record.collectionId'}/${'record.id'}/${'record.cat_image'}`; - return 'getImageUrlFrRecord(helloworld)'; + // TODO: implement getImageUrlFrRecord + return 'not implemented'; } export default function SampleTitleCard({ lpModel }: { lpModel: Customer }): React.JSX.Element { diff --git a/002_source/cms/src/app/dashboard/lesson_categories/lesson-categories-sample-data.tsx b/002_source/cms/src/app/dashboard/lesson_categories/lesson-categories-sample-data.tsx index 6a815ad..3d72108 100644 --- a/002_source/cms/src/app/dashboard/lesson_categories/lesson-categories-sample-data.tsx +++ b/002_source/cms/src/app/dashboard/lesson_categories/lesson-categories-sample-data.tsx @@ -1,5 +1,5 @@ import { dayjs } from '@/lib/dayjs'; -import { LessonCategory } from '@/components/dashboard/lesson_category/type'; +import type { LessonCategory } from '@/components/dashboard/lesson_category/type'; // import type { LessonCategory } from '@/components/dashboard/lp_categories/type'; diff --git a/002_source/cms/src/app/dashboard/lesson_categories/view/[cat_id]/page.tsx b/002_source/cms/src/app/dashboard/lesson_categories/view/[cat_id]/page.tsx index cc14c52..25c80a9 100644 --- a/002_source/cms/src/app/dashboard/lesson_categories/view/[cat_id]/page.tsx +++ b/002_source/cms/src/app/dashboard/lesson_categories/view/[cat_id]/page.tsx @@ -28,12 +28,13 @@ import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning'; import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; -import { RecordModel } from 'pocketbase'; +import type { RecordModel } from 'pocketbase'; import { useTranslation } from 'react-i18next'; import { paths } from '@/paths'; import { dayjs } from '@/lib/dayjs'; import { logger } from '@/lib/default-logger'; +import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts'; import { pb } from '@/lib/pb'; import { PropertyItem } from '@/components/core/property-item'; import { PropertyList } from '@/components/core/property-list'; @@ -45,7 +46,7 @@ import { Notifications } from '@/components/dashboard/lesson_category/notificati import { Payments } from '@/components/dashboard/lesson_category/payments'; import type { Address } from '@/components/dashboard/lesson_category/shipping-address'; import { ShippingAddress } from '@/components/dashboard/lesson_category/shipping-address'; -import { LessonCategory } from '@/components/dashboard/lesson_category/type'; +import type { LessonCategory } from '@/components/dashboard/lesson_category/type'; // import type { LessonCategory } from '@/components/dashboard/lp_categories/type'; import FormLoading from '@/components/loading'; @@ -119,39 +120,72 @@ export default function Page(): React.JSX.Element { {t('dashboard.lessonCategorys.list.title')} - - + + empty
- + {showLessonCategory.name} } + icon={ + + } label={showLessonCategory.visible} size="small" variant="outlined" /> - + {showLessonCategory.id}
-
- - + + {( [ - { key: 'Customer ID', value: }, + { + key: 'Customer ID', + value: ( + + ), + }, { key: 'Name', value: showLessonCategory.name }, { key: 'Pos', value: showLessonCategory.pos }, { @@ -195,9 +238,20 @@ export default function Page(): React.JSX.Element { { key: 'Quota', value: ( - - - + + + 50% @@ -206,7 +260,11 @@ export default function Page(): React.JSX.Element { ] satisfies { key: string; value: React.ReactNode }[] ).map( (item): React.JSX.Element => ( - + ) )}
@@ -223,11 +281,17 @@ export default function Page(): React.JSX.Element {
-
- + A deleted lesson category cannot be restored. All data will be permanently removed.
@@ -235,7 +299,10 @@ export default function Page(): React.JSX.Element {
- + }> + } @@ -294,8 +364,14 @@ export default function Page(): React.JSX.Element { title={t('billing-details', { ns: 'lesson_category' })} /> - - } sx={{ '--PropertyItem-padding': '16px' }}> + + } + sx={{ '--PropertyItem-padding': '16px' }} + > {( [ { key: 'Credit card', value: '**** 4142' }, @@ -307,7 +383,11 @@ export default function Page(): React.JSX.Element { ] satisfies { key: string; value: React.ReactNode }[] ).map( (item): React.JSX.Element => ( - + ) )} @@ -317,7 +397,10 @@ export default function Page(): React.JSX.Element { }> + } @@ -329,7 +412,10 @@ export default function Page(): React.JSX.Element { title={t('shipping-addresses', { ns: 'lesson_category' })} /> - + {( [ { @@ -351,7 +437,11 @@ export default function Page(): React.JSX.Element { }, ] satisfies Address[] ).map((address) => ( - + ))} diff --git a/002_source/cms/src/app/dashboard/lesson_types/lesson-types-data.tsx b/002_source/cms/src/app/dashboard/lesson_types/lesson-types-data.tsx index 8a6d02d..16be07b 100644 --- a/002_source/cms/src/app/dashboard/lesson_types/lesson-types-data.tsx +++ b/002_source/cms/src/app/dashboard/lesson_types/lesson-types-data.tsx @@ -1,5 +1,5 @@ import { dayjs } from '@/lib/dayjs'; -import { LessonType } from '@/components/dashboard/lesson_type/lesson-type'; +import type { LessonType } from '@/components/dashboard/lesson_type/lesson-type'; // import type { LessonType } from '@/components/dashboard/lesson_type/ILessonType'; diff --git a/002_source/cms/src/app/dashboard/lesson_types/lesson-types-sample-data.tsx b/002_source/cms/src/app/dashboard/lesson_types/lesson-types-sample-data.tsx index 63b5239..92d609b 100644 --- a/002_source/cms/src/app/dashboard/lesson_types/lesson-types-sample-data.tsx +++ b/002_source/cms/src/app/dashboard/lesson_types/lesson-types-sample-data.tsx @@ -1,5 +1,5 @@ import { dayjs } from '@/lib/dayjs'; -import { LessonType } from '@/components/dashboard/lesson_type/lesson-type'; +import type { LessonType } from '@/components/dashboard/lesson_type/lesson-type'; // import type { LessonType } from '@/components/dashboard/lesson_type/ILessonType'; diff --git a/002_source/cms/src/app/dashboard/lp/categories/list/page.tsx b/002_source/cms/src/app/dashboard/lp/categories/list/page.tsx index 82e2355..164a9f1 100644 --- a/002_source/cms/src/app/dashboard/lp/categories/list/page.tsx +++ b/002_source/cms/src/app/dashboard/lp/categories/list/page.tsx @@ -88,8 +88,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { React.useEffect(() => { if (!isFirstRun.current) { isFirstRun.current = true; - } else { - if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) { + } else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) { // reset page number as tab changes setLastListOption(listOption); setCurrentPage(0); @@ -97,7 +96,6 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { } else { void reloadRows(); } - } }, [currentPage, rowsPerPage, listOption]); React.useEffect(() => { diff --git a/002_source/cms/src/app/dashboard/lp/categories/lp-categories-sample-data.tsx b/002_source/cms/src/app/dashboard/lp/categories/lp-categories-sample-data.tsx index 0c1aa79..8054df7 100644 --- a/002_source/cms/src/app/dashboard/lp/categories/lp-categories-sample-data.tsx +++ b/002_source/cms/src/app/dashboard/lp/categories/lp-categories-sample-data.tsx @@ -1,5 +1,5 @@ import { dayjs } from '@/lib/dayjs'; -import { LessonCategory } from '@/components/dashboard/lesson_category/type'; +import type { LessonCategory } from '@/components/dashboard/lesson_category/type'; export const LpCategoriesSampleData = [ { diff --git a/002_source/cms/src/app/dashboard/lp/categories/view/[cat_id]/BasicDetailCard.tsx b/002_source/cms/src/app/dashboard/lp/categories/view/[cat_id]/BasicDetailCard.tsx index 6683056..3a1a777 100644 --- a/002_source/cms/src/app/dashboard/lp/categories/view/[cat_id]/BasicDetailCard.tsx +++ b/002_source/cms/src/app/dashboard/lp/categories/view/[cat_id]/BasicDetailCard.tsx @@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next'; import { PropertyItem } from '@/components/core/property-item'; import { PropertyList } from '@/components/core/property-list'; -import { LpCategory } from '@/components/dashboard/lp/categories/type'; +import type { LpCategory } from '@/components/dashboard/lp/categories/type'; export default function BasicDetailCard({ lpModel: model, diff --git a/002_source/cms/src/app/dashboard/lp/categories/view/[cat_id]/TitleCard.tsx b/002_source/cms/src/app/dashboard/lp/categories/view/[cat_id]/TitleCard.tsx index 6e38fb6..5dfa618 100644 --- a/002_source/cms/src/app/dashboard/lp/categories/view/[cat_id]/TitleCard.tsx +++ b/002_source/cms/src/app/dashboard/lp/categories/view/[cat_id]/TitleCard.tsx @@ -10,11 +10,8 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; import { useTranslation } from 'react-i18next'; -import { LpCategory } from '@/components/dashboard/lp/categories/type'; - -function getImageUrlFrRecord(record: LpCategory): string { - return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; -} +import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts'; +import type { LpCategory } from '@/components/dashboard/lp/categories/type'; export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): React.JSX.Element { const { t } = useTranslation(); @@ -28,7 +25,7 @@ export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): R > {t('empty')} diff --git a/002_source/cms/src/app/dashboard/lp/questions/list/page.tsx b/002_source/cms/src/app/dashboard/lp/questions/list/page.tsx index 062b71d..b6ce73f 100644 --- a/002_source/cms/src/app/dashboard/lp/questions/list/page.tsx +++ b/002_source/cms/src/app/dashboard/lp/questions/list/page.tsx @@ -88,8 +88,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { React.useEffect(() => { if (!isFirstRun.current) { isFirstRun.current = true; - } else { - if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) { + } else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) { // reset page number as tab changes setLastListOption(listOption); setCurrentPage(0); @@ -97,7 +96,6 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { } else { void reloadRows(); } - } }, [currentPage, rowsPerPage, listOption]); React.useEffect(() => { diff --git a/002_source/cms/src/app/dashboard/lp/questions/lp-categories-sample-data.tsx b/002_source/cms/src/app/dashboard/lp/questions/lp-categories-sample-data.tsx index 0c1aa79..8054df7 100644 --- a/002_source/cms/src/app/dashboard/lp/questions/lp-categories-sample-data.tsx +++ b/002_source/cms/src/app/dashboard/lp/questions/lp-categories-sample-data.tsx @@ -1,5 +1,5 @@ import { dayjs } from '@/lib/dayjs'; -import { LessonCategory } from '@/components/dashboard/lesson_category/type'; +import type { LessonCategory } from '@/components/dashboard/lesson_category/type'; export const LpCategoriesSampleData = [ { diff --git a/002_source/cms/src/app/dashboard/lp/questions/view/[cat_id]/BasicDetailCard.tsx b/002_source/cms/src/app/dashboard/lp/questions/view/[cat_id]/BasicDetailCard.tsx index 6683056..3a1a777 100644 --- a/002_source/cms/src/app/dashboard/lp/questions/view/[cat_id]/BasicDetailCard.tsx +++ b/002_source/cms/src/app/dashboard/lp/questions/view/[cat_id]/BasicDetailCard.tsx @@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next'; import { PropertyItem } from '@/components/core/property-item'; import { PropertyList } from '@/components/core/property-list'; -import { LpCategory } from '@/components/dashboard/lp/categories/type'; +import type { LpCategory } from '@/components/dashboard/lp/categories/type'; export default function BasicDetailCard({ lpModel: model, diff --git a/002_source/cms/src/app/dashboard/lp/questions/view/[cat_id]/TitleCard.tsx b/002_source/cms/src/app/dashboard/lp/questions/view/[cat_id]/TitleCard.tsx index 6e38fb6..5dfa618 100644 --- a/002_source/cms/src/app/dashboard/lp/questions/view/[cat_id]/TitleCard.tsx +++ b/002_source/cms/src/app/dashboard/lp/questions/view/[cat_id]/TitleCard.tsx @@ -10,11 +10,8 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; import { useTranslation } from 'react-i18next'; -import { LpCategory } from '@/components/dashboard/lp/categories/type'; - -function getImageUrlFrRecord(record: LpCategory): string { - return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; -} +import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts'; +import type { LpCategory } from '@/components/dashboard/lp/categories/type'; export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): React.JSX.Element { const { t } = useTranslation(); @@ -28,7 +25,7 @@ export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): R > {t('empty')} diff --git a/002_source/cms/src/app/dashboard/mf/categories/list/page.tsx b/002_source/cms/src/app/dashboard/mf/categories/list/page.tsx index 170606f..eff37ea 100644 --- a/002_source/cms/src/app/dashboard/mf/categories/list/page.tsx +++ b/002_source/cms/src/app/dashboard/mf/categories/list/page.tsx @@ -86,8 +86,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { React.useEffect(() => { if (!isFirstRun.current) { isFirstRun.current = true; - } else { - if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) { + } else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) { // reset page number as tab changes setLastListOption(listOption); setCurrentPage(0); @@ -95,7 +94,6 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { } else { void reloadRows(); } - } }, [currentPage, rowsPerPage, listOption]); React.useEffect(() => { diff --git a/002_source/cms/src/app/dashboard/mf/categories/lp-categories-sample-data.tsx b/002_source/cms/src/app/dashboard/mf/categories/lp-categories-sample-data.tsx index 0c1aa79..8054df7 100644 --- a/002_source/cms/src/app/dashboard/mf/categories/lp-categories-sample-data.tsx +++ b/002_source/cms/src/app/dashboard/mf/categories/lp-categories-sample-data.tsx @@ -1,5 +1,5 @@ import { dayjs } from '@/lib/dayjs'; -import { LessonCategory } from '@/components/dashboard/lesson_category/type'; +import type { LessonCategory } from '@/components/dashboard/lesson_category/type'; export const LpCategoriesSampleData = [ { diff --git a/002_source/cms/src/app/dashboard/mf/categories/view/[cat_id]/TitleCard.tsx b/002_source/cms/src/app/dashboard/mf/categories/view/[cat_id]/TitleCard.tsx index 059baaf..77f36d2 100644 --- a/002_source/cms/src/app/dashboard/mf/categories/view/[cat_id]/TitleCard.tsx +++ b/002_source/cms/src/app/dashboard/mf/categories/view/[cat_id]/TitleCard.tsx @@ -10,12 +10,9 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; import { useTranslation } from 'react-i18next'; +import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts'; import type { MfCategory } from '@/components/dashboard/mf/categories/type'; -function getImageUrlFrRecord(record: MfCategory): string { - return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; -} - export default function SampleTitleCard({ lpModel }: { lpModel: MfCategory }): React.JSX.Element { const { t } = useTranslation(); @@ -28,7 +25,7 @@ export default function SampleTitleCard({ lpModel }: { lpModel: MfCategory }): R > {t('empty')} diff --git a/002_source/cms/src/app/dashboard/mf/questions/list/page.tsx b/002_source/cms/src/app/dashboard/mf/questions/list/page.tsx index aded80f..aa263e1 100644 --- a/002_source/cms/src/app/dashboard/mf/questions/list/page.tsx +++ b/002_source/cms/src/app/dashboard/mf/questions/list/page.tsx @@ -86,8 +86,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { React.useEffect(() => { if (!isFirstRun.current) { isFirstRun.current = true; - } else { - if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) { + } else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) { // reset page number as tab changes setLastListOption(listOption); setCurrentPage(0); @@ -95,7 +94,6 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { } else { void reloadRows(); } - } }, [currentPage, rowsPerPage, listOption]); React.useEffect(() => { diff --git a/002_source/cms/src/app/dashboard/mf/questions/lp-categories-sample-data.tsx b/002_source/cms/src/app/dashboard/mf/questions/lp-categories-sample-data.tsx index 0c1aa79..8054df7 100644 --- a/002_source/cms/src/app/dashboard/mf/questions/lp-categories-sample-data.tsx +++ b/002_source/cms/src/app/dashboard/mf/questions/lp-categories-sample-data.tsx @@ -1,5 +1,5 @@ import { dayjs } from '@/lib/dayjs'; -import { LessonCategory } from '@/components/dashboard/lesson_category/type'; +import type { LessonCategory } from '@/components/dashboard/lesson_category/type'; export const LpCategoriesSampleData = [ { diff --git a/002_source/cms/src/app/dashboard/mf/questions/view/[cat_id]/BasicDetailCard.tsx b/002_source/cms/src/app/dashboard/mf/questions/view/[cat_id]/BasicDetailCard.tsx index 9c013b8..1e885c3 100644 --- a/002_source/cms/src/app/dashboard/mf/questions/view/[cat_id]/BasicDetailCard.tsx +++ b/002_source/cms/src/app/dashboard/mf/questions/view/[cat_id]/BasicDetailCard.tsx @@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next'; import { PropertyItem } from '@/components/core/property-item'; import { PropertyList } from '@/components/core/property-list'; -import { MfCategory } from '@/components/dashboard/mf/categories/type'; +import type { MfCategory } from '@/components/dashboard/mf/categories/type'; export default function BasicDetailCard({ lpModel: model, diff --git a/002_source/cms/src/app/dashboard/mf/questions/view/[cat_id]/TitleCard.tsx b/002_source/cms/src/app/dashboard/mf/questions/view/[cat_id]/TitleCard.tsx index 059baaf..77f36d2 100644 --- a/002_source/cms/src/app/dashboard/mf/questions/view/[cat_id]/TitleCard.tsx +++ b/002_source/cms/src/app/dashboard/mf/questions/view/[cat_id]/TitleCard.tsx @@ -10,12 +10,9 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; import { useTranslation } from 'react-i18next'; +import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts'; import type { MfCategory } from '@/components/dashboard/mf/categories/type'; -function getImageUrlFrRecord(record: MfCategory): string { - return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; -} - export default function SampleTitleCard({ lpModel }: { lpModel: MfCategory }): React.JSX.Element { const { t } = useTranslation(); @@ -28,7 +25,7 @@ export default function SampleTitleCard({ lpModel }: { lpModel: MfCategory }): R > {t('empty')} diff --git a/002_source/cms/src/app/dashboard/students/create/page.tsx b/002_source/cms/src/app/dashboard/students/create/page.tsx index c428fad..0f88255 100644 --- a/002_source/cms/src/app/dashboard/students/create/page.tsx +++ b/002_source/cms/src/app/dashboard/students/create/page.tsx @@ -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 { StudentCreateForm } from '@/components/dashboard/student/student-create-form'; -export const metadata = { title: `Create | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; - export default function Page(): React.JSX.Element { + const { t } = useTranslation(['students']); + return ( - Customers + {t('students')}
- Create customer + + {t('create-student')} + {/* */} +
diff --git a/002_source/cms/src/app/dashboard/students/edit/[id]/page.tsx b/002_source/cms/src/app/dashboard/students/edit/[id]/page.tsx index d80d10c..b9fc2c2 100644 --- a/002_source/cms/src/app/dashboard/students/edit/[id]/page.tsx +++ b/002_source/cms/src/app/dashboard/students/edit/[id]/page.tsx @@ -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'; diff --git a/002_source/cms/src/app/dashboard/students/list/page.tsx b/002_source/cms/src/app/dashboard/students/list/page.tsx index 9797d60..e1fd324 100644 --- a/002_source/cms/src/app/dashboard/students/list/page.tsx +++ b/002_source/cms/src/app/dashboard/students/list/page.tsx @@ -1,6 +1,7 @@ -// src/app/dashboard/students/list/page.tsx 'use client'; +// src/app/dashboard/students/list/page.tsx +// // RULES: // contains list page for students (Students) // @@ -15,12 +16,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 { StudentsFilters } from '@/components/dashboard/student/students-filters'; -import { StudentsPagination } from '@/components/dashboard/student/students-pagination'; -import { StudentsSelectionProvider } from '@/components/dashboard/student/students-selection-context'; -import { StudentsTable } from '@/components/dashboard/student/students-table'; -import type { Student } from '@/components/dashboard/student/type.d'; import { useTranslation } from 'react-i18next'; import { paths } from '@/paths'; @@ -29,13 +24,18 @@ import { logger } from '@/lib/default-logger'; import { pb } from '@/lib/pb'; import ErrorDisplay from '@/components/dashboard/error'; import { defaultStudent } from '@/components/dashboard/student/_constants'; +import { StudentsFilters } from '@/components/dashboard/student/students-filters'; +import { StudentsPagination } from '@/components/dashboard/student/students-pagination'; +import { StudentsSelectionProvider } from '@/components/dashboard/student/students-selection-context'; +import { StudentsTable } from '@/components/dashboard/student/students-table'; +import type { Student } from '@/components/dashboard/student/type.d'; import FormLoading from '@/components/loading'; export default function Page({ searchParams }: PageProps): React.JSX.Element { const { t } = useTranslation(['students']); const router = useRouter(); - const { email, phone, sortDir, status } = searchParams; + const { email, phone, sortDir, state } = searchParams; const [studentsData, setStudentsData] = React.useState([]); @@ -104,8 +104,8 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { const tempFilter = []; let tempSortDir = ''; - if (status) { - tempFilter.push(`status = "${status}"`); + if (state) { + tempFilter.push(`state = "${state}"`); } if (sortDir) { @@ -128,7 +128,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { preFinalListOption = { ...preFinalListOption, sort: tempSortDir }; } setListOption(preFinalListOption); - }, [sortDir, email, phone, status]); + }, [sortDir, email, phone, state]); if (showLoading) return ; @@ -176,7 +176,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { @@ -210,7 +210,7 @@ interface PageProps { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; - status?: string; + state?: string; // }; } diff --git a/002_source/cms/src/app/dashboard/students/view/[id]/TitleCard.tsx b/002_source/cms/src/app/dashboard/students/view/[id]/TitleCard.tsx index 33aba02..0834068 100644 --- a/002_source/cms/src/app/dashboard/students/view/[id]/TitleCard.tsx +++ b/002_source/cms/src/app/dashboard/students/view/[id]/TitleCard.tsx @@ -9,14 +9,10 @@ import Typography from '@mui/material/Typography'; import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; import { useTranslation } from 'react-i18next'; + +import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts'; import type { Student } from '@/components/dashboard/student/type.d'; -// import type { CrCategory } from '@/components/dashboard/cr/categories/type'; - -function getImageUrlForStudent(student: Student): string { - return `http://127.0.0.1:8090/api/files/${student.collectionId}/${student.id}/${student.avatar}`; -} - export default function SampleTitleCard({ studentRecord }: { studentRecord: Student }): React.JSX.Element { const { t } = useTranslation(); @@ -29,7 +25,7 @@ export default function SampleTitleCard({ studentRecord }: { studentRecord: Stud > {t('empty')} diff --git a/002_source/cms/src/app/dashboard/students/xxx/BasicDetailCard.tsx b/002_source/cms/src/app/dashboard/students/xxx/BasicDetailCard.tsx deleted file mode 100644 index e64427e..0000000 --- a/002_source/cms/src/app/dashboard/students/xxx/BasicDetailCard.tsx +++ /dev/null @@ -1,80 +0,0 @@ -'use client'; - -import * as React from 'react'; -import Avatar from '@mui/material/Avatar'; -import Card from '@mui/material/Card'; -import CardHeader from '@mui/material/CardHeader'; -import Chip from '@mui/material/Chip'; -import Divider from '@mui/material/Divider'; -import IconButton from '@mui/material/IconButton'; -import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; -import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; -import { useTranslation } from 'react-i18next'; - -import { PropertyItem } from '@/components/core/property-item'; -import { PropertyList } from '@/components/core/property-list'; -// import { CrCategory } from '@/components/dashboard/cr/categories/type'; -import type { Student } from '@/components/dashboard/student/type.d'; - -export default function BasicDetailCard({ - lpModel: model, - handleEditClick, -}: { - lpModel: Student; - handleEditClick: () => void; -}): React.JSX.Element { - const { t } = useTranslation(); - - return ( - - { - handleEditClick(); - }} - > - - - } - avatar={ - - - - } - title={t('list.basic-details')} - /> - } - orientation="vertical" - sx={{ '--PropertyItem-padding': '12px 24px' }} - > - {( - [ - { - key: 'Customer ID', - value: ( - - ), - }, - { key: 'Email', value: model.email }, - { key: 'Quota', value: model.quota }, - { key: 'Status', value: model.status }, - ] satisfies { key: string; value: React.ReactNode }[] - ).map( - (item): React.JSX.Element => ( - - ) - )} - - - ); -} diff --git a/002_source/cms/src/app/dashboard/students/xxx/TitleCard.tsx b/002_source/cms/src/app/dashboard/students/xxx/TitleCard.tsx deleted file mode 100644 index 9be2807..0000000 --- a/002_source/cms/src/app/dashboard/students/xxx/TitleCard.tsx +++ /dev/null @@ -1,76 +0,0 @@ -'use client'; - -import * as React from 'react'; -import { Button } from '@mui/material'; -import Avatar from '@mui/material/Avatar'; -import Chip from '@mui/material/Chip'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; -import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; -import { useTranslation } from 'react-i18next'; -import type { Student } from '@/components/dashboard/student/type.d'; - -// import type { CrCategory } from '@/components/dashboard/cr/categories/type'; - -function getImageUrlFrRecord(record: Student): string { - // TODO: fix this - // `http://127.0.0.1:8090/api/files/${'record.collectionId'}/${'record.id'}/${'record.cat_image'}`; - return 'getImageUrlFrRecord(helloworld)'; -} - -export default function SampleTitleCard({ lpModel }: { lpModel: Student }): React.JSX.Element { - const { t } = useTranslation(); - - return ( - <> - - - {t('empty')} - -
- - {lpModel.email} - - } - label={lpModel.quota} - size="small" - variant="outlined" - /> - - - {lpModel.status} - -
-
-
- -
- - ); -} diff --git a/002_source/cms/src/app/dashboard/students/xxx/page.tsx b/002_source/cms/src/app/dashboard/students/xxx/page.tsx deleted file mode 100644 index 5362910..0000000 --- a/002_source/cms/src/app/dashboard/students/xxx/page.tsx +++ /dev/null @@ -1,142 +0,0 @@ -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { useParams, useRouter } from 'next/navigation'; -import SampleAddressCard from '@/app/dashboard/Sample/AddressCard'; -import { SampleNotifications } from '@/app/dashboard/Sample/Notifications'; -import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard'; -import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard'; - -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Grid from '@mui/material/Unstable_Grid2'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import type { RecordModel } from 'pocketbase'; -import { useTranslation } from 'react-i18next'; - -import { config } from '@/config'; -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 { Notifications } from '@/components/dashboard/student/notifications'; -import FormLoading from '@/components/loading'; -import BasicDetailCard from './BasicDetailCard'; -import TitleCard from './TitleCard'; -import { defaultStudent } from '@/components/dashboard/student/_constants'; -import type { Student } from '@/components/dashboard/student/type.d'; -import { COL_STUDENTS } from '@/constants'; - -export default function Page(): React.JSX.Element { - const { t } = useTranslation(); - const router = useRouter(); - // - const { customerId } = useParams<{ customerId: string }>(); - // - const [showLoading, setShowLoading] = React.useState(true); - const [showError, setShowError] = React.useState({ show: false, detail: '' }); - // - const [showLessonCategory, setShowLessonCategory] = React.useState(defaultStudent); - - function handleEditClick(): void { - router.push(paths.dashboard.students.edit(showLessonCategory.id)); - } - - React.useEffect(() => { - if (customerId) { - pb.collection(COL_STUDENTS) - .getOne(customerId) - .then((model: RecordModel) => { - setShowLessonCategory({ ...defaultStudent, ...model }); - }) - .catch((err) => { - logger.error(err); - toast(t('list.error')); - - setShowError({ show: true, detail: JSON.stringify(err) }); - }) - .finally(() => { - setShowLoading(false); - }); - } - }, [customerId]); - - // return <>{JSON.stringify({ showError, showLessonCategory }, null, 2)}; - - if (showLoading) return ; - if (showError.show) - return ( - - ); - - return ( - - - -
- - - Students - -
- - - -
- - - - - - - - - - - - - - - -
-
- ); -} diff --git a/002_source/cms/src/app/dashboard/teachers/create/page.tsx b/002_source/cms/src/app/dashboard/teachers/create/page.tsx index 294fe48..2358537 100644 --- a/002_source/cms/src/app/dashboard/teachers/create/page.tsx +++ b/002_source/cms/src/app/dashboard/teachers/create/page.tsx @@ -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 ( - Teachers + {t('teachers')}
- Create teacher + + {t('create-teacher')} + {/* */} +
diff --git a/002_source/cms/src/app/dashboard/teachers/edit/[id]/page.tsx b/002_source/cms/src/app/dashboard/teachers/edit/[id]/page.tsx index f7bef10..3965e80 100644 --- a/002_source/cms/src/app/dashboard/teachers/edit/[id]/page.tsx +++ b/002_source/cms/src/app/dashboard/teachers/edit/[id]/page.tsx @@ -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 { diff --git a/002_source/cms/src/app/dashboard/teachers/list/page.tsx b/002_source/cms/src/app/dashboard/teachers/list/page.tsx index c16289d..a78fa80 100644 --- a/002_source/cms/src/app/dashboard/teachers/list/page.tsx +++ b/002_source/cms/src/app/dashboard/teachers/list/page.tsx @@ -1,6 +1,7 @@ -// src/app/dashboard/teachers/list/page.tsx 'use client'; +// src/app/dashboard/teachers/list/page.tsx +// // RULES: // contains list page for teachers (Teachers) // @@ -15,12 +16,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 { 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'; @@ -29,13 +24,18 @@ 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 { 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 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 { email, phone, sortDir, state } = searchParams; const [teacherData, setTeacherData] = React.useState([]); @@ -103,11 +103,11 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { }, [currentPage, rowsPerPage, listOption]); React.useEffect(() => { - let tempFilter = []; + const tempFilter = []; let tempSortDir = ''; - if (status) { - tempFilter.push(`status = "${status}"`); + if (state) { + tempFilter.push(`state = "${state}"`); } if (sortDir) { @@ -130,7 +130,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { preFinalListOption = { ...preFinalListOption, sort: tempSortDir }; } setListOption(preFinalListOption); - }, [sortDir, email, phone, status]); + }, [sortDir, email, phone, state]); if (showLoading) return ; @@ -178,7 +178,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { @@ -212,7 +212,7 @@ interface PageProps { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; - status?: string; + state?: string; // }; } diff --git a/002_source/cms/src/app/dashboard/teachers/list/page.tsx.notworking b/002_source/cms/src/app/dashboard/teachers/list/page.tsx.notworking deleted file mode 100644 index 33d7590..0000000 --- a/002_source/cms/src/app/dashboard/teachers/list/page.tsx.notworking +++ /dev/null @@ -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([]); - - const [isLoadingAddPage, setIsLoadingAddPage] = React.useState(false); - const [showLoading, setShowLoading] = React.useState(true); - const [showError, setShowError] = React.useState({ show: false, detail: '' }); - // - const [rowsPerPage, setRowsPerPage] = React.useState(5); - // - const [f, setF] = React.useState([]); - const [currentPage, setCurrentPage] = React.useState(0); - // - const [recordCount, setRecordCount] = React.useState(0); - const [listOption, setListOption] = React.useState({ filter: '' }); - const [listSort, setListSort] = React.useState({}); - - function isListOptionChanged() { - return JSON.stringify(listOption) === '{}'; - } - // - const reloadRows = async (): Promise => { - try { - const listOptionTeacherOnly = isListOptionChanged() - ? { filter: `role = "teacher"` } - : { filter: [listOption.filter, `role = "teacher"`].join(' && ') }; - - const models: ListResult = 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 ; - - if (showError.show) - return ( - - ); - - return ( - - - - - {t('list.title')} - - - { - setIsLoadingAddPage(true); - router.push(paths.dashboard.teachers.create); - }} - startIcon={} - variant="contained" - > - {t('list.add')} - - - - - - - - - - - - - - - - -
{JSON.stringify(f, null, 2)}
-
-
- ); -} - -interface PageProps { - searchParams: { - email?: string; - phone?: string; - sortDir?: 'asc' | 'desc'; - status?: string; - // - }; -} diff --git a/002_source/cms/src/app/dashboard/teachers/view/[id]/TitleCard.tsx b/002_source/cms/src/app/dashboard/teachers/view/[id]/TitleCard.tsx index 066caab..81d361f 100644 --- a/002_source/cms/src/app/dashboard/teachers/view/[id]/TitleCard.tsx +++ b/002_source/cms/src/app/dashboard/teachers/view/[id]/TitleCard.tsx @@ -9,13 +9,9 @@ import Typography from '@mui/material/Typography'; import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; import { useTranslation } from 'react-i18next'; -import { Teacher } from '@/components/dashboard/teacher/type.d'; -// import type { CrCategory } from '@/components/dashboard/cr/categories/type'; - -function getImageUrlFrRecord(teacher: Teacher): string { - return `http://127.0.0.1:8090/api/files/${teacher.collectionId}/${teacher.id}/${teacher.avatar}`; -} +import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts'; +import type { Teacher } from '@/components/dashboard/teacher/type.d'; export default function SampleTitleCard({ teacherRecord }: { teacherRecord: Teacher }): React.JSX.Element { const { t } = useTranslation(); @@ -29,7 +25,7 @@ export default function SampleTitleCard({ teacherRecord }: { teacherRecord: Teac > {t('empty')} diff --git a/002_source/cms/src/app/dashboard/user_metas/edit/[id]/_PROMPT.md b/002_source/cms/src/app/dashboard/user_metas/edit/[id]/_PROMPT.md deleted file mode 100644 index 65f2da7..0000000 --- a/002_source/cms/src/app/dashboard/user_metas/edit/[id]/_PROMPT.md +++ /dev/null @@ -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 ? diff --git a/002_source/cms/src/app/dashboard/user_metas/edit/[id]/page.tsx b/002_source/cms/src/app/dashboard/user_metas/edit/[id]/page.tsx index 22154ab..1a870e9 100644 --- a/002_source/cms/src/app/dashboard/user_metas/edit/[id]/page.tsx +++ b/002_source/cms/src/app/dashboard/user_metas/edit/[id]/page.tsx @@ -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 { + ); diff --git a/002_source/cms/src/app/dashboard/user_metas/list/page.tsx b/002_source/cms/src/app/dashboard/user_metas/list/page.tsx index 2f26e35..65cf321 100644 --- a/002_source/cms/src/app/dashboard/user_metas/list/page.tsx +++ b/002_source/cms/src/app/dashboard/user_metas/list/page.tsx @@ -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,13 +23,18 @@ 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; + const { email, phone, sortDir, state } = searchParams; const [userMetaData, setUserMetaData] = React.useState([]); @@ -97,11 +96,11 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { }, [currentPage, rowsPerPage, listOption]); React.useEffect(() => { - let tempFilter = []; + const tempFilter = []; let tempSortDir = ''; - if (status) { - tempFilter.push(`status = "${status}"`); + if (state) { + tempFilter.push(`state = "${state}"`); } if (sortDir) { @@ -124,7 +123,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { preFinalListOption = { ...preFinalListOption, sort: tempSortDir }; } setListOption(preFinalListOption); - }, [sortDir, email, phone, status]); + }, [sortDir, email, phone, state]); if (showLoading) return ; @@ -172,7 +171,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { @@ -206,7 +205,7 @@ interface PageProps { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; - status?: string; + state?: string; // }; } diff --git a/002_source/cms/src/app/dashboard/user_metas/view/[id]/BasicDetailCard.tsx b/002_source/cms/src/app/dashboard/user_metas/view/[id]/BasicDetailCard.tsx index 9c467fe..110c5fc 100644 --- a/002_source/cms/src/app/dashboard/user_metas/view/[id]/BasicDetailCard.tsx +++ b/002_source/cms/src/app/dashboard/user_metas/view/[id]/BasicDetailCard.tsx @@ -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, diff --git a/002_source/cms/src/app/dashboard/user_metas/view/[id]/TitleCard.tsx b/002_source/cms/src/app/dashboard/user_metas/view/[id]/TitleCard.tsx index 066caab..81d361f 100644 --- a/002_source/cms/src/app/dashboard/user_metas/view/[id]/TitleCard.tsx +++ b/002_source/cms/src/app/dashboard/user_metas/view/[id]/TitleCard.tsx @@ -9,13 +9,9 @@ import Typography from '@mui/material/Typography'; import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; import { useTranslation } from 'react-i18next'; -import { Teacher } from '@/components/dashboard/teacher/type.d'; -// import type { CrCategory } from '@/components/dashboard/cr/categories/type'; - -function getImageUrlFrRecord(teacher: Teacher): string { - return `http://127.0.0.1:8090/api/files/${teacher.collectionId}/${teacher.id}/${teacher.avatar}`; -} +import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts'; +import type { Teacher } from '@/components/dashboard/teacher/type.d'; export default function SampleTitleCard({ teacherRecord }: { teacherRecord: Teacher }): React.JSX.Element { const { t } = useTranslation(); @@ -29,7 +25,7 @@ export default function SampleTitleCard({ teacherRecord }: { teacherRecord: Teac > {t('empty')} diff --git a/002_source/cms/src/app/dashboard/user_metas/view/[id]/page.tsx b/002_source/cms/src/app/dashboard/user_metas/view/[id]/page.tsx index ff1f083..a792805 100644 --- a/002_source/cms/src/app/dashboard/user_metas/view/[id]/page.tsx +++ b/002_source/cms/src/app/dashboard/user_metas/view/[id]/page.tsx @@ -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(); diff --git a/002_source/cms/src/app/dashboard/vocabularies/lesson-categories-sample-data.tsx b/002_source/cms/src/app/dashboard/vocabularies/lesson-categories-sample-data.tsx index 6a815ad..3d72108 100644 --- a/002_source/cms/src/app/dashboard/vocabularies/lesson-categories-sample-data.tsx +++ b/002_source/cms/src/app/dashboard/vocabularies/lesson-categories-sample-data.tsx @@ -1,5 +1,5 @@ import { dayjs } from '@/lib/dayjs'; -import { LessonCategory } from '@/components/dashboard/lesson_category/type'; +import type { LessonCategory } from '@/components/dashboard/lesson_category/type'; // import type { LessonCategory } from '@/components/dashboard/lp_categories/type'; diff --git a/002_source/cms/src/app/dashboard/vocabularies/view/[cat_id]/page.tsx b/002_source/cms/src/app/dashboard/vocabularies/view/[cat_id]/page.tsx index 3b4f3d0..bddfef6 100644 --- a/002_source/cms/src/app/dashboard/vocabularies/view/[cat_id]/page.tsx +++ b/002_source/cms/src/app/dashboard/vocabularies/view/[cat_id]/page.tsx @@ -34,6 +34,7 @@ import { useTranslation } from 'react-i18next'; import { paths } from '@/paths'; import { dayjs } from '@/lib/dayjs'; import { logger } from '@/lib/default-logger'; +import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts'; import { pb } from '@/lib/pb'; import { PropertyItem } from '@/components/core/property-item'; import { PropertyList } from '@/components/core/property-list'; @@ -128,7 +129,11 @@ export default function Page(): React.JSX.Element { sx={{ alignItems: 'center', flex: '1 1 auto' }} > diff --git a/002_source/cms/src/components/auth/custom/OAuthProvider.tsx b/002_source/cms/src/components/auth/custom/OAuthProvider.tsx new file mode 100644 index 0000000..b9a2902 --- /dev/null +++ b/002_source/cms/src/components/auth/custom/OAuthProvider.tsx @@ -0,0 +1,5 @@ +export interface OAuthProvider { + id: 'google' | 'discord' | 'github'; + name: string; + logo: string; +} diff --git a/002_source/cms/src/components/auth/custom/oAuthProviders.tsx b/002_source/cms/src/components/auth/custom/oAuthProviders.tsx new file mode 100644 index 0000000..0b18910 --- /dev/null +++ b/002_source/cms/src/components/auth/custom/oAuthProviders.tsx @@ -0,0 +1,14 @@ +'use client'; + +import type { OAuthProvider } from './OAuthProvider'; + +// export const oAuthProviders = [ +// { id: 'google', name: 'Google', logo: '/assets/logo-google.svg' }, +// { id: 'discord', name: 'Discord', logo: '/assets/logo-discord.svg' }, +// ] satisfies OAuthProvider[]; + +export const oAuthProviders = [ + { id: 'google', name: 'Google', logo: '/assets/logo-google.svg' }, + { id: 'github', name: 'Github', logo: '/assets/logo-github.svg' }, + // { id: 'discord', name: 'Discord', logo: '/assets/logo-discord.svg' }, +] satisfies OAuthProvider[]; diff --git a/002_source/cms/src/components/auth/custom/reset-password-form.tsx b/002_source/cms/src/components/auth/custom/reset-password-form/index.tsx similarity index 54% rename from 002_source/cms/src/components/auth/custom/reset-password-form.tsx rename to 002_source/cms/src/components/auth/custom/reset-password-form/index.tsx index f2f2a0f..b9bd72c 100644 --- a/002_source/cms/src/components/auth/custom/reset-password-form.tsx +++ b/002_source/cms/src/components/auth/custom/reset-password-form/index.tsx @@ -1,10 +1,9 @@ 'use client'; import * as React from 'react'; -import RouterLink from 'next/link'; +import { useRouter } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; import Alert from '@mui/material/Alert'; -import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import FormControl from '@mui/material/FormControl'; import FormHelperText from '@mui/material/FormHelperText'; @@ -13,21 +12,28 @@ import OutlinedInput from '@mui/material/OutlinedInput'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; import { z as zod } from 'zod'; import { paths } from '@/paths'; import { authClient } from '@/lib/auth/custom/client'; -import { DynamicLogo } from '@/components/core/logo'; - -const schema = zod.object({ email: zod.string().min(1, { message: 'Email is required' }).email() }); - -type Values = zod.infer; - -const defaultValues = { email: '' } satisfies Values; export function ResetPasswordForm(): React.JSX.Element { + const { t } = useTranslation(['sign_in']); + const router = useRouter(); const [isPending, setIsPending] = React.useState(false); + const schema = zod.object({ + email: zod + .string() + .min(1, { message: t('email-is-required') }) + .email(), + }); + + type Values = zod.infer; + + const defaultValues = { email: '' } satisfies Values; + const { control, handleSubmit, @@ -54,14 +60,29 @@ export function ResetPasswordForm(): React.JSX.Element { [setError] ); + function handleBackToLoginClick(): void { + router.replace(paths.auth.custom.signIn); + } + return ( -
- - + {/* +
+ +
- Reset password + */} + {t('reset-password')}
( - Email address - + {t('email-address')} + {errors.email ? {errors.email.message} : null} )} /> {errors.root ? {errors.root.message} : null} - + + + + +
diff --git a/002_source/cms/src/components/auth/custom/sign-in-form.tsx b/002_source/cms/src/components/auth/custom/sign-in-form/index.tsx similarity index 78% rename from 002_source/cms/src/components/auth/custom/sign-in-form.tsx rename to 002_source/cms/src/components/auth/custom/sign-in-form/index.tsx index 5ab6b8b..71fb853 100644 --- a/002_source/cms/src/components/auth/custom/sign-in-form.tsx +++ b/002_source/cms/src/components/auth/custom/sign-in-form/index.tsx @@ -1,11 +1,13 @@ 'use client'; + // RULES: // refer to ticket REQ0016 for login flow - +// import * as React from 'react'; import RouterLink from 'next/link'; import { useRouter } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; import Alert from '@mui/material/Alert'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; @@ -20,6 +22,7 @@ import Typography from '@mui/material/Typography'; import { Eye as EyeIcon } from '@phosphor-icons/react/dist/ssr/Eye'; import { EyeSlash as EyeSlashIcon } from '@phosphor-icons/react/dist/ssr/EyeSlash'; import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; import { z as zod } from 'zod'; import { paths } from '@/paths'; @@ -27,30 +30,25 @@ import { authClient } from '@/lib/auth/custom/client'; import { useUser } from '@/hooks/use-user'; import { DynamicLogo } from '@/components/core/logo'; import { toast } from '@/components/core/toaster'; -import { pb } from '@/lib/pb'; -interface OAuthProvider { - id: 'google' | 'discord'; - name: string; - logo: string; -} +import type { OAuthProvider } from '../OAuthProvider'; +import { oAuthProviders } from '../oAuthProviders'; -const oAuthProviders = [ - { id: 'google', name: 'Google', logo: '/assets/logo-google.svg' }, - { id: 'discord', name: 'Discord', logo: '/assets/logo-discord.svg' }, -] satisfies OAuthProvider[]; +// interface OAuthProvider { +// id: 'google' | 'discord' | 'github'; +// name: string; +// logo: string; +// } -const schema = zod.object({ - email: zod.string().min(1, { message: 'Email is required' }).email(), - password: zod.string().min(1, { message: 'Password is required' }), -}); - -type Values = zod.infer; - -const defaultValues = { email: 'admin@123.com', password: 'admin@123.com' } satisfies Values; +// const oAuthProviders = [ +// { id: 'google', name: 'Google', logo: '/assets/logo-google.svg' }, +// { id: 'github', name: 'Github', logo: '/assets/logo-github.svg' }, +// // { id: 'discord', name: 'Discord', logo: '/assets/logo-discord.svg' }, +// ] satisfies OAuthProvider[]; export function SignInForm(): React.JSX.Element { const router = useRouter(); + const { t } = useTranslation(['sign_in']); const { checkSession } = useUser(); @@ -58,6 +56,14 @@ export function SignInForm(): React.JSX.Element { const [isPending, setIsPending] = React.useState(false); + const schema = zod.object({ + email: zod.string().min(1, { message: 'Email is required' }).email(), + password: zod.string().min(1, { message: 'Password is required' }), + }); + + type Values = zod.infer; + const defaultValues = { email: '', password: '' } satisfies Values; + const { control, handleSubmit, @@ -98,14 +104,16 @@ export function SignInForm(): React.JSX.Element { // UserProvider, for this case, will not refresh the router // After refresh, GuestGuard will handle the redirect - router.refresh(); + + // TODO: resume me + // router.refresh(); }, [checkSession, router, setError] ); return ( -
+ -
+
- Sign in + {t('sign-in')} - Don't have an account?{' '} + {t('dont-have-an-account')}{' '} - Sign up + {t('sign-up')} - + {oAuthProviders.map( (provider): React.JSX.Element => ( ) )} - or + {t('or')}
@@ -173,10 +184,12 @@ export function SignInForm(): React.JSX.Element { name="email" render={({ field }) => ( - Email address + {t('email-address')} {errors.email ? {errors.email.message} : null} @@ -187,9 +200,10 @@ export function SignInForm(): React.JSX.Element { name="password" render={({ field }) => ( - Password + {t('password')} {errors.password ? {errors.password.message} : null} )} /> {errors.root ? {errors.root.message} : null} - + {t('sign-in')} +
@@ -232,7 +249,7 @@ export function SignInForm(): React.JSX.Element { href={paths.auth.custom.resetPassword} variant="subtitle2" > - Forgot password? + {t('forgot-password')} ?
@@ -240,7 +257,7 @@ export function SignInForm(): React.JSX.Element { - user:{' '} + {t('user')}:{' '} admin@123.com {' '} - password{' '} + {t('password')}{' '} - user{' '} + {t('user')}{' '} sofia@devias.io {' '} - password{' '} + {t('password')}{' '} value, 'You must accept the terms and conditions'), -}); - -type Values = zod.infer; - -const defaultValues = { firstName: '', lastName: '', email: '', password: '', terms: false } satisfies Values; +import type { OAuthProvider } from '../OAuthProvider'; +import { oAuthProviders } from '../oAuthProviders'; export function SignUpForm(): React.JSX.Element { const router = useRouter(); + const { t } = useTranslation(['sign_in']); const { checkSession } = useUser(); const [isPending, setIsPending] = React.useState(false); + const schema = zod.object({ + firstName: zod.string().min(1, { message: t('first-name-is-required') }), + lastName: zod.string().min(1, { message: t('last-name-is-required') }), + email: zod + .string() + .min(1, { message: t('email-is-required') }) + .email(), + password: zod.string().min(6, { message: t('password-should-be-at-least-6-characters') }), + terms: zod.boolean().refine((value) => value, t('you-must-accept-the-terms-and-conditions')), + }); + type Values = zod.infer; + const defaultValues = { firstName: '', lastName: '', email: '', password: '', terms: false } satisfies Values; + const { control, handleSubmit, @@ -96,7 +94,9 @@ export function SignUpForm(): React.JSX.Element { // UserProvider, for this case, will not refresh the router // After refresh, GuestGuard will handle the redirect - router.refresh(); + + // TODO: resume me + // router.refresh(); }, [checkSession, router, setError] ); @@ -104,16 +104,32 @@ export function SignUpForm(): React.JSX.Element { return (
- - + +
- Sign up - - Already have an account?{' '} - - Sign in + {t('sign-up')} + + {t('already-have-an-account')}{' '} + + {t('sign-in')} @@ -124,7 +140,15 @@ export function SignUpForm(): React.JSX.Element { ) )}
- or + {t('or')}
( - First name + {t('first-name')}: {errors.firstName ? {errors.firstName.message} : null} @@ -157,7 +181,7 @@ export function SignUpForm(): React.JSX.Element { name="lastName" render={({ field }) => ( - Last name + {t('last-name')}: {errors.lastName ? {errors.lastName.message} : null} @@ -168,8 +192,13 @@ export function SignUpForm(): React.JSX.Element { name="email" render={({ field }) => ( - Email address - + {t('email-address')}: + {errors.email ? {errors.email.message} : null} )} @@ -179,8 +208,12 @@ export function SignUpForm(): React.JSX.Element { name="password" render={({ field }) => ( - Password - + {t('password')}: + {errors.password ? {errors.password.message} : null} )} @@ -194,7 +227,7 @@ export function SignUpForm(): React.JSX.Element { control={} label={ - I have read the terms and conditions + {t('i-have-read-the')} {t('terms-and-conditions')} } /> @@ -203,13 +236,19 @@ export function SignUpForm(): React.JSX.Element { )} /> {errors.root ? {errors.root.message} : null} - + + + {t('create-account')} +
- Created users are not persisted + {t('created-users-are-not-persisted')}
); } diff --git a/002_source/cms/src/components/auth/split-layout.tsx b/002_source/cms/src/components/auth/split-layout.tsx deleted file mode 100644 index 43bdf92..0000000 --- a/002_source/cms/src/components/auth/split-layout.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import * as React from 'react'; -import Box from '@mui/material/Box'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; - -export interface SplitLayoutProps { - children: React.ReactNode; -} - -export function SplitLayout({ children }: SplitLayoutProps): React.JSX.Element { - return ( - - - - - Welcome to Devias Kit PRO - - A professional template that comes with ready-to-use MUI components developed with one common goal in - mind, help you build faster & beautiful applications. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {children} - - - - ); -} diff --git a/002_source/cms/src/components/auth/split-layout/index.tsx b/002_source/cms/src/components/auth/split-layout/index.tsx new file mode 100644 index 0000000..5e5c7c2 --- /dev/null +++ b/002_source/cms/src/components/auth/split-layout/index.tsx @@ -0,0 +1,207 @@ +'use client'; + +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; + +import LoginAvif from './login_bg.avif'; + +export interface SplitLayoutProps { + children: React.ReactNode; +} + +export function SplitLayout({ children }: SplitLayoutProps): React.JSX.Element { + const { t } = useTranslation(['sign_in']); + + return ( + + + + + + + {t('welcome-title')} + {t('welcome-notes')} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {children} + + + + ); +} diff --git a/002_source/cms/src/components/auth/split-layout/login_bg.avif b/002_source/cms/src/components/auth/split-layout/login_bg.avif new file mode 100644 index 0000000..66024d4 Binary files /dev/null and b/002_source/cms/src/components/auth/split-layout/login_bg.avif differ diff --git a/002_source/cms/src/components/dashboard/connective/_constants.ts b/002_source/cms/src/components/dashboard/connective/_constants.ts index 1458199..7967796 100644 --- a/002_source/cms/src/components/dashboard/connective/_constants.ts +++ b/002_source/cms/src/components/dashboard/connective/_constants.ts @@ -1,6 +1,6 @@ import { dayjs } from '@/lib/dayjs'; -import { CreateForm, LessonCategory } from './type'; +import type { CreateForm, LessonCategory } from './type'; // import type { CreateForm, LessonCategory } from '../lp_categories/type'; diff --git a/002_source/cms/src/components/dashboard/connective/lesson-categories-filters.tsx b/002_source/cms/src/components/dashboard/connective/lesson-categories-filters.tsx index ec387a5..7d4a5df 100644 --- a/002_source/cms/src/components/dashboard/connective/lesson-categories-filters.tsx +++ b/002_source/cms/src/components/dashboard/connective/lesson-categories-filters.tsx @@ -26,7 +26,7 @@ import { Option } from '@/components/core/option'; // import { LessonCategory } from '../lp_categories/type'; import { useLessonCategoriesSelection } from './lesson-categories-selection-context'; -import { LessonCategory } from './type'; +import type { LessonCategory } from './type'; export interface Filters { email?: string; diff --git a/002_source/cms/src/components/dashboard/connective/lesson-categories-selection-context.tsx b/002_source/cms/src/components/dashboard/connective/lesson-categories-selection-context.tsx index 693e000..180e14b 100644 --- a/002_source/cms/src/components/dashboard/connective/lesson-categories-selection-context.tsx +++ b/002_source/cms/src/components/dashboard/connective/lesson-categories-selection-context.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { useSelection } from '@/hooks/use-selection'; import type { Selection } from '@/hooks/use-selection'; -import { LessonCategory } from './type'; +import type { LessonCategory } from './type'; // import type { LessonCategory } from '../lp_categories/type'; diff --git a/002_source/cms/src/components/dashboard/connective/lesson-categories-table.tsx b/002_source/cms/src/components/dashboard/connective/lesson-categories-table.tsx index f90667a..bc9d5e5 100644 --- a/002_source/cms/src/components/dashboard/connective/lesson-categories-table.tsx +++ b/002_source/cms/src/components/dashboard/connective/lesson-categories-table.tsx @@ -99,7 +99,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef { - // eslint-disable-next-line react-hooks/rules-of-hooks + const mapping = { active: { diff --git a/002_source/cms/src/components/dashboard/cr/categories/_constants.ts b/002_source/cms/src/components/dashboard/cr/categories/_constants.ts index d530c06..9c82fd5 100644 --- a/002_source/cms/src/components/dashboard/cr/categories/_constants.ts +++ b/002_source/cms/src/components/dashboard/cr/categories/_constants.ts @@ -1,6 +1,7 @@ import { dayjs } from '@/lib/dayjs'; -import { CrCategory, CreateFormProps } from './type'; +import type { CrCategory} from './type'; +import { CreateFormProps } from './type'; export const defaultCrCategory: CrCategory = { isEmpty: false, diff --git a/002_source/cms/src/components/dashboard/cr/categories/cr-categories-selection-context.tsx b/002_source/cms/src/components/dashboard/cr/categories/cr-categories-selection-context.tsx index fbc2620..2f14530 100644 --- a/002_source/cms/src/components/dashboard/cr/categories/cr-categories-selection-context.tsx +++ b/002_source/cms/src/components/dashboard/cr/categories/cr-categories-selection-context.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { useSelection } from '@/hooks/use-selection'; import type { Selection } from '@/hooks/use-selection'; -import { CrCategory } from './type'; +import type { CrCategory } from './type'; function noop(): void { return undefined; diff --git a/002_source/cms/src/components/dashboard/cr/categories/cr-categories-table.tsx b/002_source/cms/src/components/dashboard/cr/categories/cr-categories-table.tsx index 1c342f4..2d140ff 100644 --- a/002_source/cms/src/components/dashboard/cr/categories/cr-categories-table.tsx +++ b/002_source/cms/src/components/dashboard/cr/categories/cr-categories-table.tsx @@ -97,7 +97,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef { - // eslint-disable-next-line react-hooks/rules-of-hooks + const mapping = { active: { diff --git a/002_source/cms/src/components/dashboard/cr/questions/_constants.ts b/002_source/cms/src/components/dashboard/cr/questions/_constants.ts index 4c6d940..2f595ee 100644 --- a/002_source/cms/src/components/dashboard/cr/questions/_constants.ts +++ b/002_source/cms/src/components/dashboard/cr/questions/_constants.ts @@ -1,6 +1,7 @@ import { dayjs } from '@/lib/dayjs'; -import { CreateFormProps, CrQuestion } from './type'; +import type { CrQuestion } from './type'; +import { CreateFormProps } from './type'; export const defaultCrQuestion: CrQuestion = { isEmpty: false, diff --git a/002_source/cms/src/components/dashboard/cr/questions/cr-questions-filters.tsx b/002_source/cms/src/components/dashboard/cr/questions/cr-questions-filters.tsx index 9822163..6583fcf 100644 --- a/002_source/cms/src/components/dashboard/cr/questions/cr-questions-filters.tsx +++ b/002_source/cms/src/components/dashboard/cr/questions/cr-questions-filters.tsx @@ -24,7 +24,7 @@ import { FilterButton, FilterPopover, useFilterContext } from '@/components/core import { Option } from '@/components/core/option'; import { useCrQuestionsSelection } from './cr-questions-selection-context'; -import { CrQuestion } from './type'; +import type { CrQuestion } from './type'; export interface Filters { email?: string; diff --git a/002_source/cms/src/components/dashboard/cr/questions/cr-questions-table.tsx b/002_source/cms/src/components/dashboard/cr/questions/cr-questions-table.tsx index 12cb058..12e28af 100644 --- a/002_source/cms/src/components/dashboard/cr/questions/cr-questions-table.tsx +++ b/002_source/cms/src/components/dashboard/cr/questions/cr-questions-table.tsx @@ -97,7 +97,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef { - // eslint-disable-next-line react-hooks/rules-of-hooks + const mapping = { active: { diff --git a/002_source/cms/src/components/dashboard/customer/customers-table.tsx b/002_source/cms/src/components/dashboard/customer/customers-table.tsx index f064433..c490f28 100644 --- a/002_source/cms/src/components/dashboard/customer/customers-table.tsx +++ b/002_source/cms/src/components/dashboard/customer/customers-table.tsx @@ -89,7 +89,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef { - // eslint-disable-next-line react-hooks/rules-of-hooks + const mapping = { active: { diff --git a/002_source/cms/src/components/dashboard/error/index.tsx b/002_source/cms/src/components/dashboard/error/index.tsx index 75b2ee2..6aa3eb8 100644 --- a/002_source/cms/src/components/dashboard/error/index.tsx +++ b/002_source/cms/src/components/dashboard/error/index.tsx @@ -81,7 +81,7 @@ function ErrorDisplay({ message, code, details, severity = 'error' }: PropsError {formattedMessage} - {details && } + {details ? : null} ); diff --git a/002_source/cms/src/components/dashboard/layout/notifications-popover/index.tsx b/002_source/cms/src/components/dashboard/layout/notifications-popover/index.tsx index 5fd1a8e..6c94914 100644 --- a/002_source/cms/src/components/dashboard/layout/notifications-popover/index.tsx +++ b/002_source/cms/src/components/dashboard/layout/notifications-popover/index.tsx @@ -133,7 +133,7 @@ export function NotificationsPopover({ {t('list-is-empty')} diff --git a/002_source/cms/src/components/dashboard/layout/notifications-popover/sample-notifications.tsx b/002_source/cms/src/components/dashboard/layout/notifications-popover/sample-notifications.tsx index f2b9615..3133184 100644 --- a/002_source/cms/src/components/dashboard/layout/notifications-popover/sample-notifications.tsx +++ b/002_source/cms/src/components/dashboard/layout/notifications-popover/sample-notifications.tsx @@ -1,5 +1,5 @@ 'use client'; -import { Notification } from '@/db/Notifications/type'; +import type { Notification } from '@/db/Notifications/type'; import { dayjs } from '@/lib/dayjs'; // import type { Notification } from './type.d.tsx'; diff --git a/002_source/cms/src/components/dashboard/layout/user-popover/custom-sign-out.tsx b/002_source/cms/src/components/dashboard/layout/user-popover/custom-sign-out/index.tsx similarity index 57% rename from 002_source/cms/src/components/dashboard/layout/user-popover/custom-sign-out.tsx rename to 002_source/cms/src/components/dashboard/layout/user-popover/custom-sign-out/index.tsx index ff52e59..8613544 100644 --- a/002_source/cms/src/components/dashboard/layout/user-popover/custom-sign-out.tsx +++ b/002_source/cms/src/components/dashboard/layout/user-popover/custom-sign-out/index.tsx @@ -2,7 +2,11 @@ import * as React from 'react'; import { useRouter } from 'next/navigation'; +import { LoadingButton } from '@mui/lab'; +import { Button } from '@mui/material'; import MenuItem from '@mui/material/MenuItem'; +import { SignOut as SignOutIcon } from '@phosphor-icons/react/dist/ssr/SignOut'; +import { useTranslation } from 'react-i18next'; import { authClient } from '@/lib/auth/custom/client'; import { logger } from '@/lib/default-logger'; @@ -10,39 +14,44 @@ import { useUser } from '@/hooks/use-user'; import { toast } from '@/components/core/toaster'; export function CustomSignOut(): React.JSX.Element { + const { t } = useTranslation('sign_in'); + const { checkSession } = useUser(); + const [buttonShowLoading, setButtonShowLoading] = React.useState(false); const router = useRouter(); const handleSignOut = React.useCallback(async (): Promise => { + setButtonShowLoading(true); try { const { error } = await authClient.signOut(); - if (error) { logger.error('Sign out error', error); - toast.error('Something went wrong, unable to sign out'); + toast.error(t('something-went-wrong-unable-to-sign-out')); return; } - // Refresh the auth state await checkSession?.(); - // UserProvider, for this case, will not refresh the router and we need to do it manually router.refresh(); // After refresh, AuthGuard will handle the redirect } catch (err) { logger.error('Sign out error', err); - toast.error('Something went wrong, unable to sign out'); + toast.error(t('something-went-wrong-unable-to-sign-out')); } - }, [checkSession, router]); + }, [checkSession, router, t]); return ( - } + color="secondary" > - Sign out - + {t('sign-out')} + ); } diff --git a/002_source/cms/src/components/dashboard/layout/user-popover/user-popover.tsx b/002_source/cms/src/components/dashboard/layout/user-popover/user-popover.tsx index 5db2776..f1a1db1 100644 --- a/002_source/cms/src/components/dashboard/layout/user-popover/user-popover.tsx +++ b/002_source/cms/src/components/dashboard/layout/user-popover/user-popover.tsx @@ -16,15 +16,15 @@ import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; import type { User } from '@/types/user'; import { config } from '@/config'; import { paths } from '@/paths'; +import { authClient } from '@/lib/auth/custom/client'; import { AuthStrategy } from '@/lib/auth/strategy'; +import { logger } from '@/lib/default-logger'; import { Auth0SignOut } from './auth0-sign-out'; import { CognitoSignOut } from './cognito-sign-out'; import { CustomSignOut } from './custom-sign-out'; import { FirebaseSignOut } from './firebase-sign-out'; import { SupabaseSignOut } from './supabase-sign-out'; -import { authClient } from '@/lib/auth/custom/client'; -import { logger } from '@/lib/default-logger'; const defaultUser = { id: 'USR-000', @@ -55,7 +55,8 @@ export function UserPopover({ anchorEl, onClose, open }: UserPopoverProps): Reac void loadUserMeta(); }, []); - if (!userMeta) return <>loading; + // NOTE: delay when userMeta is null, used for sign-out + if (!userMeta) return <>; return ( + {/* NOTE: hide logo
+ */}
void): ColumnDef { - // eslint-disable-next-line react-hooks/rules-of-hooks + const mapping = { active: { diff --git a/002_source/cms/src/components/dashboard/lesson_type/lesson-type-create-form.tsx b/002_source/cms/src/components/dashboard/lesson_type/lesson-type-create-form.tsx index d5f9dd4..d6e57f2 100644 --- a/002_source/cms/src/components/dashboard/lesson_type/lesson-type-create-form.tsx +++ b/002_source/cms/src/components/dashboard/lesson_type/lesson-type-create-form.tsx @@ -37,7 +37,7 @@ import { pb } from '@/lib/pb'; import { toast } from '@/components/core/toaster'; import { LessonTypeCreateFormDefault } from './_constants'; -import { CreateForm } from './lesson-type'; +import type { CreateForm } from './lesson-type'; const schema = zod.object({ name: zod.string().min(1, 'Name is required').max(255), diff --git a/002_source/cms/src/components/dashboard/lesson_type/lesson-types-selection-context.tsx b/002_source/cms/src/components/dashboard/lesson_type/lesson-types-selection-context.tsx index ae5fd37..a38ffef 100644 --- a/002_source/cms/src/components/dashboard/lesson_type/lesson-types-selection-context.tsx +++ b/002_source/cms/src/components/dashboard/lesson_type/lesson-types-selection-context.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { useSelection } from '@/hooks/use-selection'; import type { Selection } from '@/hooks/use-selection'; -import { LessonType } from './lesson-type'; +import type { LessonType } from './lesson-type'; // import { LessonType } from './ILessonType'; diff --git a/002_source/cms/src/components/dashboard/lp/categories/_constants.ts b/002_source/cms/src/components/dashboard/lp/categories/_constants.ts index 885067e..1e11ac4 100644 --- a/002_source/cms/src/components/dashboard/lp/categories/_constants.ts +++ b/002_source/cms/src/components/dashboard/lp/categories/_constants.ts @@ -1,6 +1,7 @@ import { dayjs } from '@/lib/dayjs'; -import { CreateFormProps, LpCategory } from './type'; +import type { LpCategory } from './type'; +import { CreateFormProps } from './type'; export const defaultLpCategory: LpCategory = { isEmpty: false, diff --git a/002_source/cms/src/components/dashboard/lp/categories/lp-categories-filters.tsx b/002_source/cms/src/components/dashboard/lp/categories/lp-categories-filters.tsx index 7573cc6..ee17903 100644 --- a/002_source/cms/src/components/dashboard/lp/categories/lp-categories-filters.tsx +++ b/002_source/cms/src/components/dashboard/lp/categories/lp-categories-filters.tsx @@ -24,7 +24,7 @@ import { FilterButton, FilterPopover, useFilterContext } from '@/components/core import { Option } from '@/components/core/option'; import { useLpCategoriesSelection } from './lp-categories-selection-context'; -import { LpCategory } from './type'; +import type { LpCategory } from './type'; export interface Filters { email?: string; diff --git a/002_source/cms/src/components/dashboard/lp/categories/lp-categories-selection-context.tsx b/002_source/cms/src/components/dashboard/lp/categories/lp-categories-selection-context.tsx index a6c6502..0f79e68 100644 --- a/002_source/cms/src/components/dashboard/lp/categories/lp-categories-selection-context.tsx +++ b/002_source/cms/src/components/dashboard/lp/categories/lp-categories-selection-context.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { useSelection } from '@/hooks/use-selection'; import type { Selection } from '@/hooks/use-selection'; -import { LpCategory } from './type'; +import type { LpCategory } from './type'; function noop(): void { return undefined; diff --git a/002_source/cms/src/components/dashboard/lp/categories/lp-categories-table.tsx b/002_source/cms/src/components/dashboard/lp/categories/lp-categories-table.tsx index 26034f1..fe09ce3 100644 --- a/002_source/cms/src/components/dashboard/lp/categories/lp-categories-table.tsx +++ b/002_source/cms/src/components/dashboard/lp/categories/lp-categories-table.tsx @@ -97,7 +97,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef { - // eslint-disable-next-line react-hooks/rules-of-hooks + const mapping = { active: { diff --git a/002_source/cms/src/components/dashboard/lp/categories/mf-categories-filters.tsx b/002_source/cms/src/components/dashboard/lp/categories/mf-categories-filters.tsx index 7573cc6..ee17903 100644 --- a/002_source/cms/src/components/dashboard/lp/categories/mf-categories-filters.tsx +++ b/002_source/cms/src/components/dashboard/lp/categories/mf-categories-filters.tsx @@ -24,7 +24,7 @@ import { FilterButton, FilterPopover, useFilterContext } from '@/components/core import { Option } from '@/components/core/option'; import { useLpCategoriesSelection } from './lp-categories-selection-context'; -import { LpCategory } from './type'; +import type { LpCategory } from './type'; export interface Filters { email?: string; diff --git a/002_source/cms/src/components/dashboard/lp/categories/mf-categories-selection-context.tsx b/002_source/cms/src/components/dashboard/lp/categories/mf-categories-selection-context.tsx index a6c6502..0f79e68 100644 --- a/002_source/cms/src/components/dashboard/lp/categories/mf-categories-selection-context.tsx +++ b/002_source/cms/src/components/dashboard/lp/categories/mf-categories-selection-context.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { useSelection } from '@/hooks/use-selection'; import type { Selection } from '@/hooks/use-selection'; -import { LpCategory } from './type'; +import type { LpCategory } from './type'; function noop(): void { return undefined; diff --git a/002_source/cms/src/components/dashboard/lp/categories/mf-categories-table.tsx b/002_source/cms/src/components/dashboard/lp/categories/mf-categories-table.tsx index 26034f1..fe09ce3 100644 --- a/002_source/cms/src/components/dashboard/lp/categories/mf-categories-table.tsx +++ b/002_source/cms/src/components/dashboard/lp/categories/mf-categories-table.tsx @@ -97,7 +97,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef { - // eslint-disable-next-line react-hooks/rules-of-hooks + const mapping = { active: { diff --git a/002_source/cms/src/components/dashboard/lp/questions/_constants.ts b/002_source/cms/src/components/dashboard/lp/questions/_constants.ts index 95a4d17..894d497 100644 --- a/002_source/cms/src/components/dashboard/lp/questions/_constants.ts +++ b/002_source/cms/src/components/dashboard/lp/questions/_constants.ts @@ -1,6 +1,7 @@ import { dayjs } from '@/lib/dayjs'; -import { CreateFormProps, LpQuestion } from './type'; +import type { LpQuestion } from './type'; +import { CreateFormProps } from './type'; export const defaultLpQuestion: LpQuestion = { isEmpty: false, diff --git a/002_source/cms/src/components/dashboard/lp/questions/lp-questions-filters.tsx b/002_source/cms/src/components/dashboard/lp/questions/lp-questions-filters.tsx index d5b5486..3fa2cb0 100644 --- a/002_source/cms/src/components/dashboard/lp/questions/lp-questions-filters.tsx +++ b/002_source/cms/src/components/dashboard/lp/questions/lp-questions-filters.tsx @@ -24,7 +24,7 @@ import { FilterButton, FilterPopover, useFilterContext } from '@/components/core import { Option } from '@/components/core/option'; import { useLpQuestionsSelection } from './lp-questions-selection-context'; -import { LpQuestion } from './type'; +import type { LpQuestion } from './type'; export interface Filters { email?: string; diff --git a/002_source/cms/src/components/dashboard/lp/questions/lp-questions-table.tsx b/002_source/cms/src/components/dashboard/lp/questions/lp-questions-table.tsx index 319ff9c..d344405 100644 --- a/002_source/cms/src/components/dashboard/lp/questions/lp-questions-table.tsx +++ b/002_source/cms/src/components/dashboard/lp/questions/lp-questions-table.tsx @@ -97,7 +97,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef { - // eslint-disable-next-line react-hooks/rules-of-hooks + const mapping = { active: { diff --git a/002_source/cms/src/components/dashboard/mf/categories/_constants.ts b/002_source/cms/src/components/dashboard/mf/categories/_constants.ts index 23fb774..ea7990a 100644 --- a/002_source/cms/src/components/dashboard/mf/categories/_constants.ts +++ b/002_source/cms/src/components/dashboard/mf/categories/_constants.ts @@ -1,6 +1,7 @@ import { dayjs } from '@/lib/dayjs'; -import { CreateFormProps, MfCategory } from './type'; +import type { MfCategory } from './type'; +import { CreateFormProps } from './type'; export const defaultMfCategory: MfCategory = { isEmpty: false, diff --git a/002_source/cms/src/components/dashboard/mf/categories/mf-categories-filters.tsx b/002_source/cms/src/components/dashboard/mf/categories/mf-categories-filters.tsx index f04f0e7..bae040e 100644 --- a/002_source/cms/src/components/dashboard/mf/categories/mf-categories-filters.tsx +++ b/002_source/cms/src/components/dashboard/mf/categories/mf-categories-filters.tsx @@ -24,7 +24,7 @@ import { FilterButton, FilterPopover, useFilterContext } from '@/components/core import { Option } from '@/components/core/option'; import { useMfCategoriesSelection } from './mf-categories-selection-context'; -import { MfCategory } from './type'; +import type { MfCategory } from './type'; export interface Filters { email?: string; diff --git a/002_source/cms/src/components/dashboard/mf/categories/mf-categories-selection-context.tsx b/002_source/cms/src/components/dashboard/mf/categories/mf-categories-selection-context.tsx index 6427133..a1f6a46 100644 --- a/002_source/cms/src/components/dashboard/mf/categories/mf-categories-selection-context.tsx +++ b/002_source/cms/src/components/dashboard/mf/categories/mf-categories-selection-context.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { useSelection } from '@/hooks/use-selection'; import type { Selection } from '@/hooks/use-selection'; -import { MfCategory } from './type'; +import type { MfCategory } from './type'; function noop(): void { return undefined; diff --git a/002_source/cms/src/components/dashboard/mf/categories/mf-categories-table.tsx b/002_source/cms/src/components/dashboard/mf/categories/mf-categories-table.tsx index 3cc015c..e72f1e2 100644 --- a/002_source/cms/src/components/dashboard/mf/categories/mf-categories-table.tsx +++ b/002_source/cms/src/components/dashboard/mf/categories/mf-categories-table.tsx @@ -97,7 +97,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef { - // eslint-disable-next-line react-hooks/rules-of-hooks + const mapping = { active: { diff --git a/002_source/cms/src/components/dashboard/mf/questions/_constants.ts b/002_source/cms/src/components/dashboard/mf/questions/_constants.ts index 076328c..5c77aed 100644 --- a/002_source/cms/src/components/dashboard/mf/questions/_constants.ts +++ b/002_source/cms/src/components/dashboard/mf/questions/_constants.ts @@ -1,6 +1,7 @@ import { dayjs } from '@/lib/dayjs'; -import { CreateFormProps, MfQuestion } from './type'; +import type { MfQuestion } from './type'; +import { CreateFormProps } from './type'; export const defaultMfQuestion: MfQuestion = { isEmpty: false, diff --git a/002_source/cms/src/components/dashboard/mf/questions/mf-questions-filters.tsx b/002_source/cms/src/components/dashboard/mf/questions/mf-questions-filters.tsx index 49a85f4..29f54be 100644 --- a/002_source/cms/src/components/dashboard/mf/questions/mf-questions-filters.tsx +++ b/002_source/cms/src/components/dashboard/mf/questions/mf-questions-filters.tsx @@ -24,7 +24,7 @@ import { FilterButton, FilterPopover, useFilterContext } from '@/components/core import { Option } from '@/components/core/option'; import { useMfQuestionsSelection } from './mf-questions-selection-context'; -import { MfQuestion } from './type'; +import type { MfQuestion } from './type'; export interface Filters { email?: string; diff --git a/002_source/cms/src/components/dashboard/mf/questions/mf-questions-table.tsx b/002_source/cms/src/components/dashboard/mf/questions/mf-questions-table.tsx index 7202f0a..819e062 100644 --- a/002_source/cms/src/components/dashboard/mf/questions/mf-questions-table.tsx +++ b/002_source/cms/src/components/dashboard/mf/questions/mf-questions-table.tsx @@ -97,7 +97,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef { - // eslint-disable-next-line react-hooks/rules-of-hooks + const mapping = { active: { diff --git a/002_source/cms/src/components/dashboard/student/helloworld.tsx b/002_source/cms/src/components/dashboard/student/helloworld.tsx index 3989cb1..abc7f8a 100644 --- a/002_source/cms/src/components/dashboard/student/helloworld.tsx +++ b/002_source/cms/src/components/dashboard/student/helloworld.tsx @@ -1,3 +1,6 @@ +// PURPOSE +// T.B.A. +// const helloworld = 'helloworld'; export { helloworld }; diff --git a/002_source/cms/src/components/dashboard/student/student-create-form.tsx b/002_source/cms/src/components/dashboard/student/student-create-form.tsx index 7b471be..eb67aec 100644 --- a/002_source/cms/src/components/dashboard/student/student-create-form.tsx +++ b/002_source/cms/src/components/dashboard/student/student-create-form.tsx @@ -1,14 +1,13 @@ '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 { UpdateBillingAddressById } from '@/db/billingAddress/UpdateById'; import { createStudent } from '@/db/Students/Create'; -import { getStudentById } from '@/db/Students/GetById'; -import { UpdateStudentById } from '@/db/Students/UpdateById'; import { zodResolver } from '@hookform/resolvers/zod'; import { LoadingButton } from '@mui/lab'; // @@ -41,14 +40,10 @@ 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 FormLoading from '@/components/loading'; // import ErrorDisplay from '../../error'; -import ErrorDisplay from '../error'; -import { CreateFormProps, Student } from './type.d'; +import type { CreateFormProps } from './type.d'; // TODO: review schema const schema = zod.object({ @@ -135,11 +130,11 @@ export function StudentCreateForm(): React.JSX.Element { // } const record = await createStudent(tempCreate); - toast.success('Student created'); - // router.push(paths.dashboard.students.view(record.id)); + toast.success('student-created'); + router.push(paths.dashboard.students.view(record.id)); } catch (err) { logger.error(err); - toast.error('Failed to create Student'); + toast.error('failed-to-create-student'); } finally { setIsUpdating(false); } diff --git a/002_source/cms/src/components/dashboard/student/student-edit-form.tsx b/002_source/cms/src/components/dashboard/student/student-edit-form.tsx index 1810e24..3afe504 100644 --- a/002_source/cms/src/components/dashboard/student/student-edit-form.tsx +++ b/002_source/cms/src/components/dashboard/student/student-edit-form.tsx @@ -1,12 +1,13 @@ '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, COL_USER_METAS } from '@/constants'; import { UpdateBillingAddressById } from '@/db/billingAddress/UpdateById'; import { getStudentById } from '@/db/Students/GetById'; import { UpdateStudentById } from '@/db/Students/UpdateById'; @@ -40,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({ @@ -116,8 +118,6 @@ export function StudentEditForm(): React.JSX.Element { setIsUpdating(true); const updateData = { - avatar: values.avatar ? await base64ToFile(values.avatar) : null, - // name: values.name, email: values.email, phone: values.phone, @@ -125,16 +125,17 @@ export function StudentEditForm(): React.JSX.Element { // // billingAddress: values.billingAddress, // + taxId: values.taxId, timezone: values.timezone, language: values.language, currency: values.currency, - taxId: values.taxId, + avatar: values.avatar ? await base64ToFile(values.avatar) : null, }; try { - // await pb.collection(COL_USER_METAS).update(studentId, updateData); await UpdateStudentById(studentId, updateData); - toast.success('Student updated successfully'); + // + toast.success(t('student-updated-successfully')); router.push(paths.dashboard.students.list); if (billingAddressId) { @@ -142,7 +143,7 @@ export function StudentEditForm(): React.JSX.Element { } } catch (error) { logger.error(error); - toast.error('Failed to update student'); + toast.error(t('failed-to-update-student')); } finally { setIsUpdating(false); } @@ -176,22 +177,21 @@ export function StudentEditForm(): React.JSX.Element { setShowLoading(true); try { - const result = await getStudentById(id); + const result = (await getStudentById(id)) as unknown as Student; + // reset({ ...defaultValues, ...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 student data'); + toast.error(t('failed-to-load-student-data')); setShowError({ show: true, detail: JSON.stringify(error, null, 2) }); } finally { setShowLoading(false); @@ -365,6 +365,7 @@ export function StudentEditForm(): React.JSX.Element { )} /> + {/* */} {/* */} diff --git a/002_source/cms/src/components/dashboard/student/students-filters.tsx b/002_source/cms/src/components/dashboard/student/students-filters.tsx index 50e6ad5..e7e29ba 100644 --- a/002_source/cms/src/components/dashboard/student/students-filters.tsx +++ b/002_source/cms/src/components/dashboard/student/students-filters.tsx @@ -1,11 +1,14 @@ 'use client'; + // RULES: // T.B.A. // import * as React from 'react'; import { useRouter } from 'next/navigation'; +import GetActiveCount from '@/db/Students/GetActiveCount'; import { getAllStudentsCount } from '@/db/Students/GetAllCount'; - +import GetBlockedCount from '@/db/Students/GetBlockedCount'; +import GetPendingCount from '@/db/Students/GetPendingCount'; import Button from '@mui/material/Button'; import Chip from '@mui/material/Chip'; import Divider from '@mui/material/Divider'; @@ -21,13 +24,10 @@ import { paths } from '@/paths'; import { FilterButton } from '@/components/core/filter-button'; import { Option } from '@/components/core/option'; -import { useStudentsSelection } from './students-selection-context'; -import GetBlockedCount from '@/db/Students/GetBlockedCount'; -import GetPendingCount from '@/db/Students/GetPendingCount'; -import GetActiveCount from '@/db/Students/GetActiveCount'; -import PhoneFilterPopover from './phone-filter-popover'; import EmailFilterPopover from './email-filter-popover'; -import type { StudentFiltersProps, Filters, SortDir } from './type.d'; +import PhoneFilterPopover from './phone-filter-popover'; +import { useStudentsSelection } from './students-selection-context'; +import type { Filters, SortDir, StudentFiltersProps } from './type.d'; export function StudentsFilters({ filters = {}, @@ -37,7 +37,7 @@ export function StudentsFilters({ }: StudentFiltersProps): React.JSX.Element { const { t } = useTranslation(); - const { email, phone, status } = filters; + const { email, phone, state } = filters; const [totalCount, setTotalCount] = React.useState(0); const [activeCount, setActiveCount] = React.useState(0); @@ -76,8 +76,8 @@ export function StudentsFilters({ searchParams.set('sortDir', newSortDir); } - if (newFilters.status) { - searchParams.set('status', newFilters.status); + if (newFilters.state) { + searchParams.set('state', newFilters.state); } if (newFilters.email) { @@ -99,7 +99,7 @@ export function StudentsFilters({ const handleStatusChange = React.useCallback( (_: React.SyntheticEvent, value: string) => { - updateSearchParams({ ...filters, status: value }, sortDir); + updateSearchParams({ ...filters, state: value }, sortDir); }, [updateSearchParams, filters, sortDir] ); @@ -125,7 +125,7 @@ export function StudentsFilters({ [updateSearchParams, filters] ); - const hasFilters = status || email || phone; + const hasFilters = state || email || phone; React.useEffect(() => { const fetchCount = async (): Promise => { @@ -151,7 +151,7 @@ export function StudentsFilters({ diff --git a/002_source/cms/src/components/dashboard/student/students-pagination.tsx b/002_source/cms/src/components/dashboard/student/students-pagination.tsx index 98e17f2..124c45d 100644 --- a/002_source/cms/src/components/dashboard/student/students-pagination.tsx +++ b/002_source/cms/src/components/dashboard/student/students-pagination.tsx @@ -3,9 +3,10 @@ import * as React from 'react'; import TablePagination from '@mui/material/TablePagination'; -function noop(): void { - return undefined; -} +// TODO: remove noop +// function noop(): void { +// return undefined; +// } interface StudentsPaginationProps { count: number; @@ -30,7 +31,7 @@ export function StudentsPagination({ setPage(newPage); }; - const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + const handleChangeRowsPerPage = (event: React.ChangeEvent): void => { setRowsPerPage(parseInt(event.target.value)); // console.log(parseInt(event.target.value)); }; diff --git a/002_source/cms/src/components/dashboard/student/students-table.tsx b/002_source/cms/src/components/dashboard/student/students-table.tsx index 1a5b556..8806116 100644 --- a/002_source/cms/src/components/dashboard/student/students-table.tsx +++ b/002_source/cms/src/components/dashboard/student/students-table.tsx @@ -5,21 +5,17 @@ import RouterLink from 'next/link'; import { LoadingButton } from '@mui/lab'; import Avatar from '@mui/material/Avatar'; import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; import Chip from '@mui/material/Chip'; -import IconButton from '@mui/material/IconButton'; import LinearProgress from '@mui/material/LinearProgress'; import Link from '@mui/material/Link'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; -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 { toast } from 'sonner'; import { paths } from '@/paths'; import { dayjs } from '@/lib/dayjs'; diff --git a/002_source/cms/src/components/dashboard/student/type.d.tsx b/002_source/cms/src/components/dashboard/student/type.d.ts similarity index 58% rename from 002_source/cms/src/components/dashboard/student/type.d.tsx rename to 002_source/cms/src/components/dashboard/student/type.d.ts index 84b7a8b..e85a327 100644 --- a/002_source/cms/src/components/dashboard/student/type.d.tsx +++ b/002_source/cms/src/components/dashboard/student/type.d.ts @@ -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; + + // 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,6 +70,7 @@ export interface CreateFormProps { email: string; phone?: string; company?: string; + // // handle seperately // billingAddress?: { // country: string; @@ -32,6 +80,7 @@ export interface CreateFormProps { // line1: string; // line2?: string; // }; + // taxId?: string; timezone: string; language: string; @@ -64,8 +113,9 @@ export interface EditFormProps { // quota?: number; // status?: 'pending' | 'active' | 'blocked'; } + // RULES: filter props for student search and filtering -export interface CustomersFiltersProps { +export interface StudentFiltersProps { filters?: Filters; sortDir?: SortDir; fullData: Student[]; @@ -75,5 +125,5 @@ export interface CustomersFiltersProps { export interface Filters { email?: string; phone?: string; - status?: string; + state?: string; } diff --git a/002_source/cms/src/components/dashboard/teacher/confirm-delete-modal.tsx b/002_source/cms/src/components/dashboard/teacher/confirm-delete-modal.tsx index afbb10e..be78618 100644 --- a/002_source/cms/src/components/dashboard/teacher/confirm-delete-modal.tsx +++ b/002_source/cms/src/components/dashboard/teacher/confirm-delete-modal.tsx @@ -1,6 +1,7 @@ 'use client'; import * as React from 'react'; +import { deleteTeacher } from '@/db/Teachers/Delete'; import { LoadingButton } from '@mui/lab'; import { Button, Container, Modal, Paper } from '@mui/material'; import Avatar from '@mui/material/Avatar'; @@ -12,7 +13,6 @@ import { useTranslation } from 'react-i18next'; import { logger } from '@/lib/default-logger'; import { toast } from '@/components/core/toaster'; -import { deleteTeacher } from '@/db/Teachers/Delete'; export default function ConfirmDeleteModal({ open, @@ -105,7 +105,7 @@ export default function ConfirmDeleteModal({ { + onClick={() => { handleUserConfirmDelete(); }} loading={isDeleteing} diff --git a/002_source/cms/src/components/dashboard/teacher/teacher-create-form.tsx b/002_source/cms/src/components/dashboard/teacher/teacher-create-form.tsx index c8b4242..adda8d5 100644 --- a/002_source/cms/src/components/dashboard/teacher/teacher-create-form.tsx +++ b/002_source/cms/src/components/dashboard/teacher/teacher-create-form.tsx @@ -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 { - 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 type { 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; 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(false); + const [showLoading, setShowLoading] = React.useState(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 => { + // Use standard create method from db/Customers/Create + 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 { - // Use standard create method from db/Customers/Create - const record = await createTeacher(values); - toast.success('Customer created'); + 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} > - Account information + {t('create.basic-info')} - Avatar - Min 400x400px, PNG or JPEG + {t('create.avatar')} + {t('create.avatarRequirements')} - Email address + {t('create.email-address')} - Phone number + {t('create.phone-number')} {errors.phone ? {errors.phone.message} : null} @@ -268,7 +301,10 @@ export function TeacherCreateForm(): React.JSX.Element { fullWidth > Company - + {errors.company ? {errors.company.message} : null} )} @@ -276,8 +312,9 @@ export function TeacherCreateForm(): React.JSX.Element { + {/* */} - Billing information + {t('create.billing-information')} Country {errors.billingAddress?.country ? ( {errors.billingAddress?.country?.message} @@ -362,7 +401,7 @@ export function TeacherCreateForm(): React.JSX.Element { error={Boolean(errors.billingAddress?.zipCode)} fullWidth > - Zip code + {t('create.zip-code')} {errors.billingAddress?.zipCode ? ( {errors.billingAddress?.zipCode?.message} @@ -383,7 +422,7 @@ export function TeacherCreateForm(): React.JSX.Element { error={Boolean(errors.billingAddress?.line1)} fullWidth > - Address + {t('create.address-line-1')} {errors.billingAddress?.line1 ? ( {errors.billingAddress?.line1?.message} @@ -424,7 +463,7 @@ export function TeacherCreateForm(): React.JSX.Element { /> - Additional information + {t('create.additional-information')} Timezone {errors.timezone ? {errors.timezone.message} : null} @@ -467,10 +510,11 @@ export function TeacherCreateForm(): React.JSX.Element { > Language {errors.language ? {errors.language.message} : null} @@ -489,12 +533,12 @@ export function TeacherCreateForm(): React.JSX.Element { error={Boolean(errors.currency)} fullWidth > - Currency + {t('create.currency')} {errors.currency ? {errors.currency.message} : null} @@ -511,14 +555,17 @@ export function TeacherCreateForm(): React.JSX.Element { component={RouterLink} href={paths.dashboard.teachers.list} > - Cancel + {t('create.cancelButton')} - + {t('create.updateButton')} + diff --git a/002_source/cms/src/components/dashboard/teacher/teacher-edit-form.tsx b/002_source/cms/src/components/dashboard/teacher/teacher-edit-form.tsx index 9bee1da..d22e1d9 100644 --- a/002_source/cms/src/components/dashboard/teacher/teacher-edit-form.tsx +++ b/002_source/cms/src/components/dashboard/teacher/teacher-edit-form.tsx @@ -1,12 +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_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'; // @@ -37,14 +41,15 @@ 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 { 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), @@ -89,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 }>(); // @@ -97,6 +102,7 @@ export function TeacherEditForm(): React.JSX.Element { const [showLoading, setShowLoading] = React.useState(false); // const [showError, setShowError] = React.useState({ show: false, detail: '' }); + const [billingAddressId, setBillingAddressId] = React.useState(null); const { control, @@ -116,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, @@ -125,17 +133,22 @@ 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); } }, - [teacherId, router] + [teacherId, router, t] ); const avatarInputRef = React.useRef(null); @@ -164,27 +177,28 @@ 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); } }, - [reset, setValue] + + [reset, setValue, t] ); React.useEffect(() => { @@ -301,7 +315,7 @@ export function TeacherEditForm(): React.JSX.Element { error={Boolean(errors.email)} fullWidth > - Email + {t('edit.email-address')} - Phone + {t('edit.phone-number')} {errors.phone ? {errors.phone.message} : null} @@ -352,11 +366,12 @@ export function TeacherEditForm(): React.JSX.Element { )} /> + {/* */} {/* */} - Billing Information + {t('edit.billing-information')} Country {errors.billingAddress?.country ? ( {errors.billingAddress.country.message} @@ -440,7 +458,7 @@ export function TeacherEditForm(): React.JSX.Element { error={Boolean(errors.billingAddress?.zipCode)} fullWidth > - Zip Code + {t('edit.zip-code')} {errors.billingAddress?.zipCode ? ( {errors.billingAddress.zipCode.message} @@ -461,7 +479,7 @@ export function TeacherEditForm(): React.JSX.Element { error={Boolean(errors.billingAddress?.line1)} fullWidth > - Address Line 1 + {t('edit.address-line-1')} {errors.billingAddress?.line1 ? ( {errors.billingAddress.line1.message} @@ -496,7 +514,7 @@ export function TeacherEditForm(): React.JSX.Element { - Additional Information + {t('edit.additional-information')} Language {errors.language ? {errors.language.message} : null} @@ -564,8 +584,9 @@ export function TeacherEditForm(): React.JSX.Element { error={Boolean(errors.currency)} fullWidth > - Currency + {t('edit.currency')}
diff --git a/002_source/cms/src/components/dashboard/teacher/teachers-pagination.tsx b/002_source/cms/src/components/dashboard/teacher/teachers-pagination.tsx index 5aea10d..ec05e27 100644 --- a/002_source/cms/src/components/dashboard/teacher/teachers-pagination.tsx +++ b/002_source/cms/src/components/dashboard/teacher/teachers-pagination.tsx @@ -3,9 +3,10 @@ import * as React from 'react'; import TablePagination from '@mui/material/TablePagination'; -function noop(): void { - return undefined; -} +// TODO: remove noop +// function noop(): void { +// return undefined; +// } interface CustomersPaginationProps { count: number; @@ -30,7 +31,7 @@ export function TeachersPagination({ setPage(newPage); }; - const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + const handleChangeRowsPerPage = (event: React.ChangeEvent): void => { setRowsPerPage(parseInt(event.target.value)); // console.log(parseInt(event.target.value)); }; diff --git a/002_source/cms/src/components/dashboard/teacher/teachers-table.tsx b/002_source/cms/src/components/dashboard/teacher/teachers-table.tsx index f1c4bb2..2790360 100644 --- a/002_source/cms/src/components/dashboard/teacher/teachers-table.tsx +++ b/002_source/cms/src/components/dashboard/teacher/teachers-table.tsx @@ -1,25 +1,26 @@ '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'; import Avatar from '@mui/material/Avatar'; import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; import Chip from '@mui/material/Chip'; -import IconButton from '@mui/material/IconButton'; import LinearProgress from '@mui/material/LinearProgress'; import Link from '@mui/material/Link'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; -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 { toast } from 'sonner'; import { paths } from '@/paths'; import { dayjs } from '@/lib/dayjs'; @@ -213,7 +214,6 @@ export function TeachersTable({ rows, reloadRows }: TeachersTableProps): React.J sx={{ textAlign: 'center' }} variant="body2" > - {/* TODO: update this */} {t('no-teachers-found')} diff --git a/002_source/cms/src/components/dashboard/teacher/type.d.tsx b/002_source/cms/src/components/dashboard/teacher/type.d.tsx index 6033a60..f651322 100644 --- a/002_source/cms/src/components/dashboard/teacher/type.d.tsx +++ b/002_source/cms/src/components/dashboard/teacher/type.d.tsx @@ -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; } // RULES: form data structure for editing existing teacher @@ -77,8 +79,10 @@ export interface TeachersFiltersProps { sortDir?: SortDir; fullData: Teacher[]; } + +// RULES: available filter options for student data export interface Filters { email?: string; phone?: string; - status?: string; + state?: string; } diff --git a/002_source/cms/src/components/dashboard/user.de/_GUIDELINES.md b/002_source/cms/src/components/dashboard/user.de/_GUIDELINES.md deleted file mode 100644 index 9e5a150..0000000 --- a/002_source/cms/src/components/dashboard/user.de/_GUIDELINES.md +++ /dev/null @@ -1,25 +0,0 @@ -# GUIDELINES & KEY COMPONENTS - -- `_constants.ts` contains the constant for - - - default value (defaultValue) - - empty value (emptyValue) - -- `users-table.tsx` - -- `confirm-delete-modal.tsx` - delete modal component when click delete button on list - - - `users-filters.tsx` - - `users-pagination.tsx` - - `email-filter-popover.tsx` - - `phone-filter-popover.tsx` - - `users-selection-context.tsx` - -- `user-create-form.tsx` - form to create a new user -- `user-edit-form.tsx` - form to edit a existing user - -- `type.d.tsx` - contains type definition - -- `notifications.tsx` - constants used for demonstration -- `payments.tsx` - constants used for demonstration -- `shipping-address.tsx` - constants used for demonstration diff --git a/002_source/cms/src/components/dashboard/user.de/_constants.ts b/002_source/cms/src/components/dashboard/user.de/_constants.ts deleted file mode 100644 index 62097e2..0000000 --- a/002_source/cms/src/components/dashboard/user.de/_constants.ts +++ /dev/null @@ -1,21 +0,0 @@ -// RULES: -// default variable value for customer -// empty valur for customer - -import { dayjs } from '@/lib/dayjs'; -import type { User } from './type.d'; - -export const defaultUser: User = { - id: '', - name: '', - avatar: undefined, - email: '', - phone: undefined, - quota: 0, - status: 'pending', - createdAt: dayjs().toDate(), -}; - -export const emptyLpCategory: User = { - ...defaultUser, -}; diff --git a/002_source/cms/src/components/dashboard/user.de/confirm-delete-modal.tsx b/002_source/cms/src/components/dashboard/user.de/confirm-delete-modal.tsx deleted file mode 100644 index 3a4f946..0000000 --- a/002_source/cms/src/components/dashboard/user.de/confirm-delete-modal.tsx +++ /dev/null @@ -1,124 +0,0 @@ -'use client'; - -import * as React from 'react'; -import { LoadingButton } from '@mui/lab'; -import { Button, Container, Modal, Paper } from '@mui/material'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { Note as NoteIcon } from '@phosphor-icons/react/dist/ssr/Note'; -import { useTranslation } from 'react-i18next'; - -import { logger } from '@/lib/default-logger'; -import { toast } from '@/components/core/toaster'; -import { deleteUser } from '@/db/Users/Delete'; - -export default function ConfirmDeleteModal({ - open, - setOpen, - idToDelete, - reloadRows, -}: { - open: boolean; - setOpen: (b: boolean) => void; - idToDelete: string; - reloadRows: () => void; -}): React.JSX.Element { - const { t } = useTranslation(); - - // const handleClose = () => setOpen(false); - function handleClose(): void { - setOpen(false); - } - - const [isDeleteing, setIsDeleteing] = React.useState(false); - const style = { - position: 'absolute', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - }; - - function handleUserConfirmDelete(): void { - if (idToDelete) { - setIsDeleteing(true); - - // RULES: delete - deleteUser(idToDelete) - .then(() => { - reloadRows(); - handleClose(); - toast(t('delete.success')); - }) - .catch((err) => { - // console.error(err) - logger.error(err); - toast(t('delete.error')); - }) - .finally(() => { - setIsDeleteing(false); - }); - } - } - - return ( -
- - - - - - - - - - - {t('Delete User ?')} - - {t('Are you sure you want to delete this user ?')} - - - - - { - handleUserConfirmDelete(); - }} - loading={isDeleteing} - > - {t('Delete')} - - - - - - - - -
- ); -} diff --git a/002_source/cms/src/components/dashboard/user.de/email-filter-popover.tsx b/002_source/cms/src/components/dashboard/user.de/email-filter-popover.tsx deleted file mode 100644 index 2636af0..0000000 --- a/002_source/cms/src/components/dashboard/user.de/email-filter-popover.tsx +++ /dev/null @@ -1,50 +0,0 @@ -'use client'; - -import * as React from 'react'; - -import Button from '@mui/material/Button'; -import FormControl from '@mui/material/FormControl'; -import OutlinedInput from '@mui/material/OutlinedInput'; - -import { FilterPopover, useFilterContext } from '@/components/core/filter-button'; - -// EmailFilterPopover -> email-filter-popover.tsx -export default function EmailFilterPopover(): React.JSX.Element { - const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); - const [value, setValue] = React.useState(''); - - React.useEffect(() => { - setValue((initialValue as string | undefined) ?? ''); - }, [initialValue]); - - return ( - - - { - setValue(event.target.value); - }} - onKeyUp={(event) => { - if (event.key === 'Enter') { - onApply(value); - } - }} - value={value} - /> - - - - ); -} diff --git a/002_source/cms/src/components/dashboard/user.de/helloworld.tsx b/002_source/cms/src/components/dashboard/user.de/helloworld.tsx deleted file mode 100644 index 3989cb1..0000000 --- a/002_source/cms/src/components/dashboard/user.de/helloworld.tsx +++ /dev/null @@ -1,3 +0,0 @@ -const helloworld = 'helloworld'; - -export { helloworld }; diff --git a/002_source/cms/src/components/dashboard/user.de/notifications.tsx b/002_source/cms/src/components/dashboard/user.de/notifications.tsx deleted file mode 100644 index a6c16bd..0000000 --- a/002_source/cms/src/components/dashboard/user.de/notifications.tsx +++ /dev/null @@ -1,101 +0,0 @@ -'use client'; - -import * as React from 'react'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import CardHeader from '@mui/material/CardHeader'; -import Chip from '@mui/material/Chip'; -import Select from '@mui/material/Select'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple'; - -import { dayjs } from '@/lib/dayjs'; -import { DataTable } from '@/components/core/data-table'; -import type { ColumnDef } from '@/components/core/data-table'; -import { Option } from '@/components/core/option'; - -export interface Notification { - id: string; - type: string; - status: 'delivered' | 'pending' | 'failed'; - createdAt: Date; -} - -const columns = [ - { - formatter: (row): React.JSX.Element => ( - - {row.type} - - ), - name: 'Type', - width: '300px', - }, - { - formatter: (row): React.JSX.Element => { - const mapping = { - delivered: { label: 'Delivered', color: 'success' }, - pending: { label: 'Pending', color: 'warning' }, - failed: { label: 'Failed', color: 'error' }, - } as const; - const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; - - return ; - }, - name: 'Status', - width: '200px', - }, - { - formatter: (row): React.JSX.Element => ( - - {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} - - ), - name: 'Date', - align: 'right', - }, -] satisfies ColumnDef[]; - -export interface NotificationsProps { - notifications: Notification[]; -} - -export function Notifications({ notifications }: NotificationsProps): React.JSX.Element { - return ( - - - -
- } - title="Notifications" - /> - - - - -
- -
-
- - - columns={columns} rows={notifications} /> - - -
-
-
- ); -} diff --git a/002_source/cms/src/components/dashboard/user.de/payments.tsx b/002_source/cms/src/components/dashboard/user.de/payments.tsx deleted file mode 100644 index 0420d32..0000000 --- a/002_source/cms/src/components/dashboard/user.de/payments.tsx +++ /dev/null @@ -1,138 +0,0 @@ -'use client'; - -import * as React from 'react'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import CardHeader from '@mui/material/CardHeader'; -import Chip from '@mui/material/Chip'; -import Divider from '@mui/material/Divider'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; -import { ShoppingCartSimple as ShoppingCartSimpleIcon } from '@phosphor-icons/react/dist/ssr/ShoppingCartSimple'; - -import { dayjs } from '@/lib/dayjs'; -import type { ColumnDef } from '@/components/core/data-table'; -import { DataTable } from '@/components/core/data-table'; - -export interface Payment { - currency: string; - amount: number; - invoiceId: string; - status: 'pending' | 'completed' | 'canceled' | 'refunded'; - createdAt: Date; -} - -const columns = [ - { - formatter: (row): React.JSX.Element => ( - - {new Intl.NumberFormat('en-US', { style: 'currency', currency: row.currency }).format(row.amount)} - - ), - name: 'Amount', - width: '200px', - }, - { - formatter: (row): React.JSX.Element => { - const mapping = { - pending: { label: 'Pending', color: 'warning' }, - completed: { label: 'Completed', color: 'success' }, - canceled: { label: 'Canceled', color: 'error' }, - refunded: { label: 'Refunded', color: 'error' }, - } as const; - const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; - - return ; - }, - name: 'Status', - width: '200px', - }, - { - formatter: (row): React.JSX.Element => { - return {row.invoiceId}; - }, - name: 'Invoice ID', - width: '150px', - }, - { - formatter: (row): React.JSX.Element => ( - - {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} - - ), - name: 'Date', - align: 'right', - }, -] satisfies ColumnDef[]; - -export interface PaymentsProps { - ordersValue: number; - payments: Payment[]; - refundsValue: number; - totalOrders: number; -} - -export function Payments({ ordersValue, payments = [], refundsValue, totalOrders }: PaymentsProps): React.JSX.Element { - return ( - - }> - Create Payment - - } - avatar={ - - - - } - title="Payments" - /> - - - - } - spacing={3} - sx={{ justifyContent: 'space-between', p: 2 }} - > -
- - Total orders - - {new Intl.NumberFormat('en-US').format(totalOrders)} -
-
- - Orders value - - - {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(ordersValue)} - -
-
- - Refunds - - - {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(refundsValue)} - -
-
-
- - - columns={columns} rows={payments} /> - - -
-
-
- ); -} diff --git a/002_source/cms/src/components/dashboard/user.de/phone-filter-popover.tsx b/002_source/cms/src/components/dashboard/user.de/phone-filter-popover.tsx deleted file mode 100644 index 08cbad7..0000000 --- a/002_source/cms/src/components/dashboard/user.de/phone-filter-popover.tsx +++ /dev/null @@ -1,50 +0,0 @@ -'use client'; - -import * as React from 'react'; - -import Button from '@mui/material/Button'; -import FormControl from '@mui/material/FormControl'; -import OutlinedInput from '@mui/material/OutlinedInput'; - -import { FilterPopover, useFilterContext } from '@/components/core/filter-button'; - -// phone-filter-popover.tsx -export default function PhoneFilterPopover(): React.JSX.Element { - const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); - const [value, setValue] = React.useState(''); - - React.useEffect(() => { - setValue((initialValue as string | undefined) ?? ''); - }, [initialValue]); - - return ( - - - { - setValue(event.target.value); - }} - onKeyUp={(event) => { - if (event.key === 'Enter') { - onApply(value); - } - }} - value={value} - /> - - - - ); -} diff --git a/002_source/cms/src/components/dashboard/user.de/shipping-address.tsx b/002_source/cms/src/components/dashboard/user.de/shipping-address.tsx deleted file mode 100644 index 8793e5c..0000000 --- a/002_source/cms/src/components/dashboard/user.de/shipping-address.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from 'react'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import Chip from '@mui/material/Chip'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; - -export interface Address { - id: string; - country: string; - state: string; - city: string; - zipCode: string; - street: string; - primary?: boolean; -} - -export interface ShippingAddressProps { - address: Address; -} - -export function ShippingAddress({ address }: ShippingAddressProps): React.ReactElement { - return ( - - - - - {address.street}, -
- {address.city}, {address.state}, {address.country}, -
- {address.zipCode} -
- - {address.primary ? : } - - -
-
-
- ); -} diff --git a/002_source/cms/src/components/dashboard/user.de/type.d.tsx b/002_source/cms/src/components/dashboard/user.de/type.d.tsx deleted file mode 100644 index 847764a..0000000 --- a/002_source/cms/src/components/dashboard/user.de/type.d.tsx +++ /dev/null @@ -1,69 +0,0 @@ -'use client'; - -export type SortDir = 'asc' | 'desc'; - -export interface User { - id: string; - name: string; - avatar?: string; - email: string; - phone?: string; - quota: number; - status: 'pending' | 'active' | 'blocked'; - createdAt: Date; - updatedAt?: Date; -} - -export interface CreateFormProps { - name: string; - email: string; - phone?: string; - company?: string; - billingAddress?: { - country: string; - state: string; - city: string; - zipCode: string; - line1: string; - line2?: string; - }; - taxId?: string; - timezone: string; - language: string; - currency: string; - avatar?: string; - // quota?: number; - // status?: 'pending' | 'active' | 'blocked'; -} - -export interface EditFormProps { - name: string; - email: string; - phone?: string; - company?: string; - billingAddress?: { - country: string; - state: string; - city: string; - zipCode: string; - line1: string; - line2?: string; - }; - taxId?: string; - timezone: string; - language: string; - currency: string; - avatar?: string; - // quota?: number; - // status?: 'pending' | 'active' | 'blocked'; -} -export interface CustomersFiltersProps { - filters?: Filters; - sortDir?: SortDir; - fullData: User[]; -} -export interface Filters { - email?: string; - phone?: string; - status?: string; -} diff --git a/002_source/cms/src/components/dashboard/user.de/user-create-form.tsx b/002_source/cms/src/components/dashboard/user.de/user-create-form.tsx deleted file mode 100644 index 7762893..0000000 --- a/002_source/cms/src/components/dashboard/user.de/user-create-form.tsx +++ /dev/null @@ -1,529 +0,0 @@ -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { useRouter } from 'next/navigation'; -import { zodResolver } from '@hookform/resolvers/zod'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardActions from '@mui/material/CardActions'; -import CardContent from '@mui/material/CardContent'; -import Checkbox from '@mui/material/Checkbox'; -import Divider from '@mui/material/Divider'; -import FormControl from '@mui/material/FormControl'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import FormHelperText from '@mui/material/FormHelperText'; -import InputLabel from '@mui/material/InputLabel'; -import OutlinedInput from '@mui/material/OutlinedInput'; -import Select from '@mui/material/Select'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import Grid from '@mui/material/Unstable_Grid2'; -import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; -import { Controller, useForm } from 'react-hook-form'; -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 as createUser } from '@/db/Customers/Create'; -import isDevelopment from '@/lib/check-is-development'; - -function fileToBase64(file: Blob): Promise { - 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')); - }; - }); -} - -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), - billingAddress: zod.object({ - country: zod.string().min(1, 'Country is required').max(255), - state: zod.string().min(1, 'State is required').max(255), - city: zod.string().min(1, 'City is required').max(255), - zipCode: zod.string().min(1, 'Zip code is required').max(255), - line1: zod.string().min(1, 'Street line 1 is required').max(255), - line2: zod.string().max(255).optional(), - }), - taxId: zod.string().max(255).optional(), - 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), -}); - -type Values = zod.infer; - -const defaultValues = { - avatar: '', - name: 'new name', - email: '123@123.com', - phone: '91234567', - company: '', - billingAddress: { - country: 'US', - state: '00000', - city: 'NY', - zipCode: '00000', - line1: 'test line 1', - line2: 'test line 2', - }, - taxId: '12345', - timezone: 'new_york', - language: 'en', - currency: 'USD', -} satisfies Values; - -export function UserCreateForm(): React.JSX.Element { - const router = useRouter(); - - const { - control, - handleSubmit, - formState: { errors }, - setValue, - watch, - } = useForm({ defaultValues, resolver: zodResolver(schema) }); - - const onSubmit = React.useCallback( - async (values: Values): Promise => { - try { - // Use standard create method from db/Customers/Create - const record = await createUser(values); - toast.success('User created successfully'); - router.push(paths.dashboard.user_metas.details(record.id)); - } catch (err) { - logger.error(err); - toast.error('Failed to create user. Please try again.'); - } - }, - [router] - ); - - const avatarInputRef = React.useRef(null); - const avatar = watch('avatar'); - - const handleAvatarChange = React.useCallback( - async (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - - if (file) { - const url = await fileToBase64(file); - setValue('avatar', url); - } - }, - [setValue] - ); - - return ( -
- - - } - spacing={4} - > - - Account information - - - - - - - - - - Avatar - Min 400x400px, PNG or JPEG - - - - - - - ( - - Name - - {errors.name ? {errors.name.message} : null} - - )} - /> - - - ( - - Email address - - {errors.email ? {errors.email.message} : null} - - )} - /> - - - ( - - Phone number - - {errors.phone ? {errors.phone.message} : null} - - )} - /> - - - ( - - Company - - {errors.company ? {errors.company.message} : null} - - )} - /> - - - - - Billing information - - - ( - - Country - - {errors.billingAddress?.country ? ( - {errors.billingAddress?.country?.message} - ) : null} - - )} - /> - - - ( - - State - - {errors.billingAddress?.state ? ( - {errors.billingAddress?.state?.message} - ) : null} - - )} - /> - - - ( - - City - - {errors.billingAddress?.city ? ( - {errors.billingAddress?.city?.message} - ) : null} - - )} - /> - - - ( - - Zip code - - {errors.billingAddress?.zipCode ? ( - {errors.billingAddress?.zipCode?.message} - ) : null} - - )} - /> - - - ( - - Address - - {errors.billingAddress?.line1 ? ( - {errors.billingAddress?.line1?.message} - ) : null} - - )} - /> - - - ( - - Tax ID - - {errors.taxId ? {errors.taxId.message} : null} - - )} - /> - - - - - Shipping information - } - label="Same as billing address" - /> - - - Additional information - - - ( - - Timezone - - {errors.timezone ? {errors.timezone.message} : null} - - )} - /> - - - ( - - Language - - {errors.language ? {errors.language.message} : null} - - )} - /> - - - ( - - Currency - - {errors.currency ? {errors.currency.message} : null} - - )} - /> - - - - - - - - - - - -
{JSON.stringify({ errors }, null, 2)}
-
-
- ); -} diff --git a/002_source/cms/src/components/dashboard/user.de/user-edit-form.tsx b/002_source/cms/src/components/dashboard/user.de/user-edit-form.tsx deleted file mode 100644 index a4fb402..0000000 --- a/002_source/cms/src/components/dashboard/user.de/user-edit-form.tsx +++ /dev/null @@ -1,604 +0,0 @@ -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { useParams, useRouter } from 'next/navigation'; -// -import { COL_USERS } from '@/constants'; -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'; -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 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 { 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({ - 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(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), - city: zod.string().min(1, 'City is required').max(255), - zipCode: zod.string().min(1, 'Zip code is required').max(255), - line1: zod.string().min(1, 'Street line 1 is required').max(255), - line2: zod.string().max(255).optional(), - }), - taxId: zod.string().max(255).optional(), - 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; - -const defaultValues = { - name: '', - email: '', - phone: '', - company: '', - billingAddress: { - country: '', - state: '', - city: '', - zipCode: '', - line1: '', - line2: '', - }, - taxId: '', - timezone: '', - language: '', - currency: '', - avatar: '', -} satisfies Values; - -export function UserEditForm(): React.JSX.Element { - const router = useRouter(); - const { t } = useTranslation(['lp_categories']); - - const { id: userId } = useParams<{ userId: string }>(); - // - const [isUpdating, setIsUpdating] = React.useState(false); - const [showLoading, setShowLoading] = React.useState(false); - // - const [showError, setShowError] = React.useState({ show: false, detail: '' }); - - const { - control, - handleSubmit, - formState: { errors }, - setValue, - reset, - watch, - } = useForm({ defaultValues, resolver: zodResolver(schema) }); - - const onSubmit = React.useCallback( - async (values: Values): Promise => { - setIsUpdating(true); - - const updateData = { - name: values.name, - email: values.email, - phone: values.phone, - company: values.company, - billingAddress: values.billingAddress, - taxId: values.taxId, - timezone: values.timezone, - language: values.language, - currency: values.currency, - avatar: values.avatar ? await base64ToFile(values.avatar) : null, - }; - - try { - await pb.collection(COL_USERS).update(userId, updateData); - toast.success('User updated successfully'); - router.push(paths.dashboard.user_metas.list); - } catch (error) { - logger.error(error); - toast.error('Failed to update user'); - } finally { - setIsUpdating(false); - } - }, - [userId, router] - ); - - const avatarInputRef = React.useRef(null); - const avatar = watch('avatar'); - - const handleAvatarChange = React.useCallback( - async (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - - if (file) { - const url = await fileToBase64(file); - setValue('avatar', url); - } - }, - [setValue] - ); - - // TODO: need to align with save form - // use trycatch - const [textDescription, setTextDescription] = React.useState(''); - const [textRemarks, setTextRemarks] = React.useState(''); - - // load existing data when user arrive - const loadExistingData = React.useCallback( - async (id: string) => { - setShowLoading(true); - - try { - const result = await pb.collection(COL_USERS).getOne(id); - 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}` - ); - const blob = await fetchResult.blob(); - const url = await fileToBase64(blob); - setValue('avatar', url); - } - } catch (error) { - logger.error(error); - toast.error('Failed to load user 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 ; - if (showError.show) - return ( - - ); - - return ( -
- - - } - spacing={4} - > - - {t('edit.basic-info')} - - - - - - - - - - {t('edit.avatar')} - {t('edit.avatarRequirements')} - - - - - - - ( - - Name - - {errors.name ? {errors.name.message} : null} - - )} - /> - - - ( - - Email - - {errors.email ? {errors.email.message} : null} - - )} - /> - - - ( - - Phone - - {errors.phone ? {errors.phone.message} : null} - - )} - /> - - - ( - - Company - - {errors.company ? {errors.company.message} : null} - - )} - /> - - - - {/* */} - - Billing Information - - - ( - - Country - - {errors.billingAddress?.country ? ( - {errors.billingAddress.country.message} - ) : null} - - )} - /> - - - ( - - State - - {errors.billingAddress?.state ? ( - {errors.billingAddress.state.message} - ) : null} - - )} - /> - - - ( - - City - - {errors.billingAddress?.city ? ( - {errors.billingAddress.city.message} - ) : null} - - )} - /> - - - ( - - Zip Code - - {errors.billingAddress?.zipCode ? ( - {errors.billingAddress.zipCode.message} - ) : null} - - )} - /> - - - ( - - Address Line 1 - - {errors.billingAddress?.line1 ? ( - {errors.billingAddress.line1.message} - ) : null} - - )} - /> - - - ( - - Tax ID - - {errors.taxId ? {errors.taxId.message} : null} - - )} - /> - - - - - - Additional Information - - - ( - - Timezone - - {errors.timezone ? {errors.timezone.message} : null} - - )} - /> - - - ( - - Language - - {errors.language ? {errors.language.message} : null} - - )} - /> - - - ( - - Currency - - {errors.currency ? {errors.currency.message} : null} - - )} - /> - - - - - - - - - - {t('edit.updateButton')} - - - - -
{JSON.stringify({ errors }, null, 2)}
-
-
- ); -} diff --git a/002_source/cms/src/components/dashboard/user.de/users-filters.tsx b/002_source/cms/src/components/dashboard/user.de/users-filters.tsx deleted file mode 100644 index 418c7eb..0000000 --- a/002_source/cms/src/components/dashboard/user.de/users-filters.tsx +++ /dev/null @@ -1,242 +0,0 @@ -'use client'; -// RULES: -// T.B.A. -// -import * as React from 'react'; -import { useRouter } from 'next/navigation'; -import { getAllCustomersCount } from '@/db/Customers/GetAllCount'; - -import Button from '@mui/material/Button'; -import Chip from '@mui/material/Chip'; -import Divider from '@mui/material/Divider'; -import Select from '@mui/material/Select'; -import type { SelectChangeEvent } from '@mui/material/Select'; -import Stack from '@mui/material/Stack'; -import Tab from '@mui/material/Tab'; -import Tabs from '@mui/material/Tabs'; -import Typography from '@mui/material/Typography'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { FilterButton } from '@/components/core/filter-button'; -import { Option } from '@/components/core/option'; - -import { useCustomersSelection } from './users-selection-context'; -import GetBlockedCount from '@/db/Customers/GetBlockedCount'; -import GetPendingCount from '@/db/Customers/GetPendingCount'; -import GetActiveCount from '@/db/Customers/GetActiveCount'; -import PhoneFilterPopover from './phone-filter-popover'; -import EmailFilterPopover from './email-filter-popover'; -import type { CustomersFiltersProps, Filters, SortDir } from './type.d'; - -export function UsersFilters({ filters = {}, sortDir = 'desc', fullData }: CustomersFiltersProps): React.JSX.Element { - const { t } = useTranslation(); - - const { email, phone, status } = filters; - - const [totalCount, setTotalCount] = React.useState(0); - const [activeCount, setActiveCount] = React.useState(0); - const [pendingCount, setPendingCount] = React.useState(0); - const [blockedCount, setBlockedCount] = React.useState(0); - - const router = useRouter(); - - const selection = useCustomersSelection(); - - // function getVisible(): number { - // return fullData.reduce((count, item: CrQuestion) => { - // return item.visible === 'visible' ? count + 1 : count; - // }, 0); - // } - - // function getHidden(): number { - // return fullData.reduce((count, item: CrQuestion) => { - // return item.visible === 'hidden' ? count + 1 : count; - // }, 0); - // } - - // The tabs should be generated using API data. - const tabs = [ - { label: 'All', value: '', count: totalCount }, - { label: 'Active', value: 'active', count: activeCount }, - { label: 'Pending', value: 'pending', count: pendingCount }, - { label: 'Blocked', value: 'blocked', count: blockedCount }, - ] as const; - - const updateSearchParams = React.useCallback( - (newFilters: Filters, newSortDir: SortDir): void => { - const searchParams = new URLSearchParams(); - - if (newSortDir === 'asc') { - searchParams.set('sortDir', newSortDir); - } - - if (newFilters.status) { - searchParams.set('status', newFilters.status); - } - - if (newFilters.email) { - searchParams.set('email', newFilters.email); - } - - if (newFilters.phone) { - searchParams.set('phone', newFilters.phone); - } - - router.push(`${paths.dashboard.customers.list}?${searchParams.toString()}`); - }, - [router] - ); - - const handleClearFilters = React.useCallback(() => { - updateSearchParams({}, sortDir); - }, [updateSearchParams, sortDir]); - - const handleStatusChange = React.useCallback( - (_: React.SyntheticEvent, value: string) => { - updateSearchParams({ ...filters, status: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); - - const handleEmailChange = React.useCallback( - (value?: string) => { - updateSearchParams({ ...filters, email: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); - - const handlePhoneChange = React.useCallback( - (value?: string) => { - updateSearchParams({ ...filters, phone: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); - - const handleSortChange = React.useCallback( - (event: SelectChangeEvent) => { - updateSearchParams(filters, event.target.value as SortDir); - }, - [updateSearchParams, filters] - ); - - const hasFilters = status || email || phone; - - React.useEffect(() => { - const fetchCount = async (): Promise => { - try { - const tc = await getAllCustomersCount(); - setTotalCount(tc); - - const bc = await GetBlockedCount(); - setBlockedCount(bc); - const pc = await GetPendingCount(); - setPendingCount(pc); - const ac = await GetActiveCount(); - setActiveCount(ac); - } catch (error) { - // - } - }; - void fetchCount(); - }, []); - - return ( -
- - {tabs.map((tab) => ( - - } - iconPosition="end" - key={tab.value} - label={tab.label} - sx={{ minHeight: 'auto' }} - tabIndex={0} - value={tab.value} - /> - ))} - - - - - { - handleEmailChange(value as string); - }} - onFilterDelete={() => { - handleEmailChange(); - }} - popover={} - value={email} - /> - - { - handlePhoneChange(value as string); - }} - onFilterDelete={() => { - handlePhoneChange(); - }} - popover={} - value={phone} - /> - - {hasFilters ? : null} - - {selection.selectedAny ? ( - - - {selection.selected.size} selected - - - - ) : null} - - -
- ); -} diff --git a/002_source/cms/src/components/dashboard/user.de/users-pagination.tsx b/002_source/cms/src/components/dashboard/user.de/users-pagination.tsx deleted file mode 100644 index 1d92ade..0000000 --- a/002_source/cms/src/components/dashboard/user.de/users-pagination.tsx +++ /dev/null @@ -1,50 +0,0 @@ -'use client'; - -import * as React from 'react'; -import TablePagination from '@mui/material/TablePagination'; - -function noop(): void { - return undefined; -} - -interface UsersPaginationProps { - count: number; - page: number; - // - setPage: (page: number) => void; - setRowsPerPage: (page: number) => void; - rowsPerPage: number; -} - -export function UsersPagination({ - count, - page, - // - setPage, - setRowsPerPage, - rowsPerPage, -}: UsersPaginationProps): React.JSX.Element { - // You should implement the pagination using a similar logic as the filters. - // Note that when page change, you should keep the filter search params. - const handleChangePage = (event: unknown, newPage: number) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event: React.ChangeEvent) => { - setRowsPerPage(parseInt(event.target.value)); - // console.log(parseInt(event.target.value)); - }; - - return ( - - ); -} diff --git a/002_source/cms/src/components/dashboard/user.de/users-selection-context.tsx b/002_source/cms/src/components/dashboard/user.de/users-selection-context.tsx deleted file mode 100644 index f2a5c1e..0000000 --- a/002_source/cms/src/components/dashboard/user.de/users-selection-context.tsx +++ /dev/null @@ -1,40 +0,0 @@ -'use client'; - -import * as React from 'react'; - -import { useSelection } from '@/hooks/use-selection'; -import type { Selection } from '@/hooks/use-selection'; - -import type { User } from './type.d'; - -function noop(): void { - return undefined; -} - -export interface CustomersSelectionContextValue extends Selection {} - -export const CustomersSelectionContext = React.createContext({ - deselectAll: noop, - deselectOne: noop, - selectAll: noop, - selectOne: noop, - selected: new Set(), - selectedAny: false, - selectedAll: false, -}); - -interface UsersSelectionProviderProps { - children: React.ReactNode; - users: User[]; -} - -export function UsersSelectionProvider({ children, users = [] }: UsersSelectionProviderProps): React.JSX.Element { - const customerIds = React.useMemo(() => users.map((customer) => customer.id), [users]); - const selection = useSelection(customerIds); - - return {children}; -} - -export function useCustomersSelection(): CustomersSelectionContextValue { - return React.useContext(CustomersSelectionContext); -} diff --git a/002_source/cms/src/components/dashboard/user.de/users-table.tsx b/002_source/cms/src/components/dashboard/user.de/users-table.tsx deleted file mode 100644 index 8e13573..0000000 --- a/002_source/cms/src/components/dashboard/user.de/users-table.tsx +++ /dev/null @@ -1,222 +0,0 @@ -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { LoadingButton } from '@mui/lab'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Chip from '@mui/material/Chip'; -import IconButton from '@mui/material/IconButton'; -import LinearProgress from '@mui/material/LinearProgress'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; -import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; -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 { toast } from 'sonner'; - -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 { useCustomersSelection } from './users-selection-context'; -import type { User } from './type.d'; - -function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] { - return [ - { - formatter: (row): React.JSX.Element => ( - - {' '} -
- - {row.name} - - - {row.email} - -
-
- ), - name: 'Name', - width: '250px', - }, - { - formatter: (row): React.JSX.Element => ( - - - - {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} - - - ), - name: 'Quota', - width: '150px', - }, - { field: 'phone', name: 'Phone number', width: '150px' }, - - { - formatter: (row): React.JSX.Element => { - // eslint-disable-next-line react-hooks/rules-of-hooks - - const mapping = { - active: { - label: 'Active', - icon: ( - - ), - }, - blocked: { label: 'Blocked', icon: }, - pending: { - label: 'Pending', - icon: ( - - ), - }, - } as const; - const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null }; - - return ( - - ); - }, - name: 'Status', - width: '150px', - }, - { - formatter(row) { - return dayjs(row.createdAt).format('MMM D, YYYY'); - }, - name: 'Created at', - width: '150px', - }, - { - formatter: (row): React.JSX.Element => ( - - - - - { - handleDeleteClick(row.id); - }} - > - - - - ), - name: 'Actions', - hideName: true, - align: 'right', - }, - ]; -} - -export interface UsersTableProps { - rows: User[]; - reloadRows: () => void; -} - -export function UsersTable({ rows, reloadRows }: UsersTableProps): React.JSX.Element { - const { t } = useTranslation(['customers']); - const { deselectAll, deselectOne, selectAll, selectOne, selected } = useCustomersSelection(); - - const [idToDelete, setIdToDelete] = React.useState(''); - const [open, setOpen] = React.useState(false); - - function handleDeleteClick(testId: string): void { - setOpen(true); - setIdToDelete(testId); - } - - return ( - - - - columns={columns(handleDeleteClick)} - onDeselectAll={deselectAll} - onDeselectOne={(_, row) => { - deselectOne(row.id); - }} - onSelectAll={selectAll} - onSelectOne={(_, row) => { - selectOne(row.id); - }} - rows={rows} - selectable - selected={selected} - /> - {!rows.length ? ( - - - {/* TODO: update this */} - {t('no-record-found')} - - - ) : null} - - ); -} diff --git a/002_source/cms/src/components/dashboard/user_meta/_constants.ts b/002_source/cms/src/components/dashboard/user_meta/_constants.ts index 47de880..8e7264d 100644 --- a/002_source/cms/src/components/dashboard/user_meta/_constants.ts +++ b/002_source/cms/src/components/dashboard/user_meta/_constants.ts @@ -3,6 +3,7 @@ // empty valur for customer import { dayjs } from '@/lib/dayjs'; + import type { UserMeta } from './type.d'; export const defaultUserMeta: UserMeta = { diff --git a/002_source/cms/src/components/dashboard/user_meta/confirm-delete-modal.tsx b/002_source/cms/src/components/dashboard/user_meta/confirm-delete-modal.tsx index 55d5ddf..70094ef 100644 --- a/002_source/cms/src/components/dashboard/user_meta/confirm-delete-modal.tsx +++ b/002_source/cms/src/components/dashboard/user_meta/confirm-delete-modal.tsx @@ -1,6 +1,7 @@ 'use client'; import * as React from 'react'; +import { deleteUserMeta } from '@/db/UserMetas/Delete'; import { LoadingButton } from '@mui/lab'; import { Button, Container, Modal, Paper } from '@mui/material'; import Avatar from '@mui/material/Avatar'; @@ -12,7 +13,6 @@ import { useTranslation } from 'react-i18next'; import { logger } from '@/lib/default-logger'; import { toast } from '@/components/core/toaster'; -import { deleteUserMeta } from '@/db/UserMetas/Delete'; export default function ConfirmDeleteModal({ open, @@ -105,7 +105,7 @@ export default function ConfirmDeleteModal({ { + onClick={() => { handleUserConfirmDelete(); }} loading={isDeleteing} diff --git a/002_source/cms/src/components/dashboard/user_meta/type.d.tsx b/002_source/cms/src/components/dashboard/user_meta/type.d.ts similarity index 80% rename from 002_source/cms/src/components/dashboard/user_meta/type.d.tsx rename to 002_source/cms/src/components/dashboard/user_meta/type.d.ts index 231568a..16cf98b 100644 --- a/002_source/cms/src/components/dashboard/user_meta/type.d.tsx +++ b/002_source/cms/src/components/dashboard/user_meta/type.d.ts @@ -1,26 +1,11 @@ -'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'; -// obsoleted -// 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 DBUserMeta { name: string; // @@ -50,7 +35,7 @@ export interface DBUserMeta { collectionId: string; } -// RULES: core teacher data structure +// RULES: core user meta data structure export interface UserMeta { name: string; // @@ -72,7 +57,6 @@ export interface UserMeta { timezone: string; language: string; currency: string; - // id: string; createdAt: Date; @@ -80,12 +64,14 @@ export interface UserMeta { 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; @@ -94,6 +80,7 @@ export interface CreateFormProps { line1: string; line2?: string; }; + // taxId?: string; timezone: string; language: string; @@ -103,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; @@ -126,14 +113,15 @@ 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; - status?: string; + state?: string; } diff --git a/002_source/cms/src/components/dashboard/user_meta/user-activation-edit-form.tsx b/002_source/cms/src/components/dashboard/user_meta/user-activation-edit-form.tsx new file mode 100644 index 0000000..dc65c7a --- /dev/null +++ b/002_source/cms/src/components/dashboard/user_meta/user-activation-edit-form.tsx @@ -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; + +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(false); + const [showLoading, setShowLoading] = React.useState(false); + // + const [showError, setShowError] = React.useState({ show: false, detail: '' }); + + const { + control, + handleSubmit, + formState: { errors }, + setValue, + reset, + watch, + } = useForm({ defaultValues, resolver: zodResolver(schema) }); + + const onSubmit = React.useCallback( + async (values: Values): Promise => { + 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(''); + const [textRemarks, setTextRemarks] = React.useState(''); + + // 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 ; + if (showError.show) + return ( + + ); + + return ( +
+ + + } + spacing={4} + > + ( + + + {t('user-activation')} {t('optional')} + + + {errors.verified ? {errors.verified.message} : null} + + )} + /> + + + +
+ + + + {t('edit.updateButton')} + +
+
+
+ +
{JSON.stringify({ errors }, null, 2)}
+
+
+ ); +} diff --git a/002_source/cms/src/components/dashboard/user_meta/user-meta-edit-form.tsx b/002_source/cms/src/components/dashboard/user_meta/user-meta-edit-form.tsx index aee8a3a..7393743 100644 --- a/002_source/cms/src/components/dashboard/user_meta/user-meta-edit-form.tsx +++ b/002_source/cms/src/components/dashboard/user_meta/user-meta-edit-form.tsx @@ -1,12 +1,13 @@ '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_CUSTOMERS, COL_USER_METAS } from '@/constants'; import { UpdateBillingAddressById } from '@/db/billingAddress/UpdateById'; import { getUserMetaById } from '@/db/UserMetas/GetById'; import { UpdateUserMetaById } from '@/db/UserMetas/UpdateById'; @@ -40,14 +41,15 @@ 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 { 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), @@ -92,7 +94,7 @@ const defaultValues = { export function UserMetaEditForm(): React.JSX.Element { const router = useRouter(); - const { t } = useTranslation(['lp_categories']); + const { t } = useTranslation(['user_metas']); const { id: userMetaId } = useParams<{ id: string }>(); // @@ -100,6 +102,7 @@ export function UserMetaEditForm(): React.JSX.Element { const [showLoading, setShowLoading] = React.useState(false); // const [showError, setShowError] = React.useState({ show: false, detail: '' }); + const [billingAddressId, setBillingAddressId] = React.useState(null); const { control, @@ -119,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, @@ -128,12 +133,17 @@ export function UserMetaEditForm(): React.JSX.Element { }; try { - await pb.collection(COL_USER_METAS).update(userMetaId, 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); } @@ -167,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); @@ -304,7 +314,7 @@ export function UserMetaEditForm(): React.JSX.Element { error={Boolean(errors.email)} fullWidth > - Email + {t('edit.email-address')} - Phone + {t('edit.phone-number')} {errors.phone ? {errors.phone.message} : null} @@ -355,11 +365,12 @@ export function UserMetaEditForm(): React.JSX.Element { )} /> + {/* */} {/* */} - Billing Information + {t('edit.billing-information')} Country {errors.billingAddress?.country ? ( {errors.billingAddress.country.message} @@ -443,7 +457,7 @@ export function UserMetaEditForm(): React.JSX.Element { error={Boolean(errors.billingAddress?.zipCode)} fullWidth > - Zip Code + {t('edit.zip-code')} {errors.billingAddress?.zipCode ? ( {errors.billingAddress.zipCode.message} @@ -464,7 +478,7 @@ export function UserMetaEditForm(): React.JSX.Element { error={Boolean(errors.billingAddress?.line1)} fullWidth > - Address Line 1 + {t('edit.address-line-1')} {errors.billingAddress?.line1 ? ( {errors.billingAddress.line1.message} @@ -499,7 +513,7 @@ export function UserMetaEditForm(): React.JSX.Element { - Additional Information + {t('edit.additional-information')} Language {errors.language ? {errors.language.message} : null} @@ -567,8 +583,9 @@ export function UserMetaEditForm(): React.JSX.Element { error={Boolean(errors.currency)} fullWidth > - Currency + {t('edit.currency')} diff --git a/002_source/cms/src/components/dashboard/user_meta/user-metas-pagination.tsx b/002_source/cms/src/components/dashboard/user_meta/user-metas-pagination.tsx index 986e6c9..fe855d6 100644 --- a/002_source/cms/src/components/dashboard/user_meta/user-metas-pagination.tsx +++ b/002_source/cms/src/components/dashboard/user_meta/user-metas-pagination.tsx @@ -3,9 +3,10 @@ import * as React from 'react'; import TablePagination from '@mui/material/TablePagination'; -function noop(): void { - return undefined; -} +// TODO remove noop +// function noop(): void { +// return undefined; +// } interface UserMetasPaginationProps { count: number; @@ -30,7 +31,7 @@ export function UserMetasPagination({ setPage(newPage); }; - const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + const handleChangeRowsPerPage = (event: React.ChangeEvent): void => { setRowsPerPage(parseInt(event.target.value)); // console.log(parseInt(event.target.value)); }; diff --git a/002_source/cms/src/components/dashboard/user_meta/user-metas-table.tsx b/002_source/cms/src/components/dashboard/user_meta/user-metas-table.tsx index 2129a1e..25c0dee 100644 --- a/002_source/cms/src/components/dashboard/user_meta/user-metas-table.tsx +++ b/002_source/cms/src/components/dashboard/user_meta/user-metas-table.tsx @@ -1,31 +1,35 @@ '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'; import Avatar from '@mui/material/Avatar'; import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; import Chip from '@mui/material/Chip'; -import IconButton from '@mui/material/IconButton'; import LinearProgress from '@mui/material/LinearProgress'; import Link from '@mui/material/Link'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; -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 getImageUrlFromFile from '@/lib/get-image-url-from-file.ts'; 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[] { return [ @@ -37,7 +41,7 @@ function columns(handleDeleteClick: (userMetaId: string) => void): ColumnDef
@@ -168,6 +172,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 +212,7 @@ export function UserMetasTable({ rows, reloadRows }: UserMetasTableProps): React sx={{ textAlign: 'center' }} variant="body2" > - No user metadata found + {t('no-user-meta-found')} ) : null} diff --git a/002_source/cms/src/components/dashboard/vocabulary/_constants.ts b/002_source/cms/src/components/dashboard/vocabulary/_constants.ts index a7cba1d..3fb3d67 100644 --- a/002_source/cms/src/components/dashboard/vocabulary/_constants.ts +++ b/002_source/cms/src/components/dashboard/vocabulary/_constants.ts @@ -1,5 +1,4 @@ -import { dayjs } from '@/lib/dayjs'; -import { Vocabulary, CreateForm } from './type'; +import type { CreateForm, Vocabulary } from './type'; export const defaultVocabulary: Vocabulary = { id: 'default-vocabulary-id', diff --git a/002_source/cms/src/components/dashboard/vocabulary/confirm-delete-modal.tsx b/002_source/cms/src/components/dashboard/vocabulary/confirm-delete-modal.tsx index 671b235..7a391d6 100644 --- a/002_source/cms/src/components/dashboard/vocabulary/confirm-delete-modal.tsx +++ b/002_source/cms/src/components/dashboard/vocabulary/confirm-delete-modal.tsx @@ -1,8 +1,7 @@ 'use client'; import * as React from 'react'; -import { useRouter } from 'next/navigation'; -import { COL_LESSON_TYPES } from '@/constants'; +import deleteVocabulary from '@/db/Vocabularies/Delete'; import { LoadingButton } from '@mui/lab'; import { Button, Container, Modal, Paper } from '@mui/material'; import Avatar from '@mui/material/Avatar'; @@ -10,14 +9,15 @@ import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; import { Note as NoteIcon } from '@phosphor-icons/react/dist/ssr/Note'; -import PocketBase from 'pocketbase'; +// TODO: remove import +// import PocketBase from 'pocketbase'; import { useTranslation } from 'react-i18next'; import { logger } from '@/lib/default-logger'; import { toast } from '@/components/core/toaster'; -import deleteVocabulary from '@/db/Vocabularies/Delete'; -const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL); +// TODO: remove pb +// const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL); export default function ConfirmDeleteModal({ open, @@ -110,7 +110,7 @@ export default function ConfirmDeleteModal({ { + onClick={() => { handleUserConfirmDelete(); }} loading={isDeleteing} diff --git a/002_source/cms/src/components/dashboard/vocabulary/type.d.ts b/002_source/cms/src/components/dashboard/vocabulary/type.d.ts index 1506e8e..e309b4a 100644 --- a/002_source/cms/src/components/dashboard/vocabulary/type.d.ts +++ b/002_source/cms/src/components/dashboard/vocabulary/type.d.ts @@ -1,10 +1,9 @@ // RULES: // should match the collection `Vocabularies` from `schema.dbml` export interface Vocabulary { - id: string; created?: string; updated?: string; - image?: string; + image: string; sound?: string; word?: string; word_c?: string; @@ -23,6 +22,9 @@ export interface Vocabulary { // }; }; + // + id: string; + collectionId: string; } // RULES: for use with vocabulary-create-form.tsx diff --git a/002_source/cms/src/components/dashboard/vocabulary/vocabularies-filters.tsx b/002_source/cms/src/components/dashboard/vocabulary/vocabularies-filters.tsx index 83c8354..74e61fc 100644 --- a/002_source/cms/src/components/dashboard/vocabulary/vocabularies-filters.tsx +++ b/002_source/cms/src/components/dashboard/vocabulary/vocabularies-filters.tsx @@ -2,9 +2,12 @@ import * as React from 'react'; import { useRouter } from 'next/navigation'; +import { listLessonCategories } from '@/db/LessonCategories/ListLessonCategories'; import GetAllCount from '@/db/Vocabularies/GetAllCount'; import GetHiddenCount from '@/db/Vocabularies/GetHiddenCount'; import GetVisibleCount from '@/db/Vocabularies/GetVisibleCount'; +// import { listLessonCategories } from '@/db/LessonCategories/ListLessonCategories'; +import { MenuItem } from '@mui/material'; import Button from '@mui/material/Button'; import Chip from '@mui/material/Chip'; import Divider from '@mui/material/Divider'; @@ -19,14 +22,12 @@ import Typography from '@mui/material/Typography'; import { useTranslation } from 'react-i18next'; import { paths } from '@/paths'; +import { logger } from '@/lib/default-logger'; import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; import { Option } from '@/components/core/option'; -import { useVocabulariesSelection } from './vocabularies-selection-context'; import type { Vocabulary } from './type'; -import { listLessonCategories } from '@/db/LessonCategories/listLessonCategories'; -import { MenuItem } from '@mui/material'; -import { logger } from '@/lib/default-logger'; +import { useVocabulariesSelection } from './vocabularies-selection-context'; export interface Filters { email?: string; @@ -50,6 +51,7 @@ export interface VocabulariesFiltersProps { export function VocabulariesFilters({ filters = {}, sortDir = 'desc', + // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars fullData, }: VocabulariesFiltersProps): React.JSX.Element { const { t } = useTranslation(); @@ -119,12 +121,13 @@ export function VocabulariesFilters({ updateSearchParams({}, sortDir); }, [updateSearchParams, sortDir]); - const handleStatusChange = React.useCallback( - (_: React.SyntheticEvent, value: string) => { - updateSearchParams({ ...filters, status: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); + // TODO: remove me + // const handleStatusChange = React.useCallback( + // (_: React.SyntheticEvent, value: string) => { + // updateSearchParams({ ...filters, status: value }, sortDir); + // }, + // [updateSearchParams, filters, sortDir] + // ); const handleVisibleChange = React.useCallback( (_: React.SyntheticEvent, value: string) => { @@ -161,19 +164,19 @@ export function VocabulariesFilters({ [updateSearchParams, filters, sortDir] ); - const handleEmailChange = React.useCallback( - (value?: string) => { - updateSearchParams({ ...filters, email: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); + // const handleEmailChange = React.useCallback( + // (value?: string) => { + // updateSearchParams({ ...filters, email: value }, sortDir); + // }, + // [updateSearchParams, filters, sortDir] + // ); - const handlePhoneChange = React.useCallback( - (value?: string) => { - updateSearchParams({ ...filters, phone: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); + // const handlePhoneChange = React.useCallback( + // (value?: string) => { + // updateSearchParams({ ...filters, phone: value }, sortDir); + // }, + // [updateSearchParams, filters, sortDir] + // ); const handleSortChange = React.useCallback( (event: SelectChangeEvent) => { @@ -185,7 +188,7 @@ export function VocabulariesFilters({ const [allCategories, setAllCategories] = React.useState<{ value: string; label: string }[]>([]); async function listAllCategories() { try { - let result = await listLessonCategories(); + const result = await listLessonCategories(); const tempAllCategories = result.map((c) => { return { value: c.id, @@ -568,42 +571,43 @@ function EmailFilterPopover(): React.JSX.Element { ); } -function PhoneFilterPopover(): React.JSX.Element { - const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); - const [value, setValue] = React.useState(''); +// remove PhoneFilterPopover +// function PhoneFilterPopover(): React.JSX.Element { +// const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); +// const [value, setValue] = React.useState(''); - React.useEffect(() => { - setValue((initialValue as string | undefined) ?? ''); - }, [initialValue]); +// React.useEffect(() => { +// setValue((initialValue as string | undefined) ?? ''); +// }, [initialValue]); - return ( - - - { - setValue(event.target.value); - }} - onKeyUp={(event) => { - if (event.key === 'Enter') { - onApply(value); - } - }} - value={value} - /> - - - - ); -} +// return ( +// +// +// { +// setValue(event.target.value); +// }} +// onKeyUp={(event) => { +// if (event.key === 'Enter') { +// onApply(value); +// } +// }} +// value={value} +// /> +// +// +// +// ); +// } diff --git a/002_source/cms/src/components/dashboard/vocabulary/vocabularies-pagination.tsx b/002_source/cms/src/components/dashboard/vocabulary/vocabularies-pagination.tsx index b7256e4..1dcf2f6 100644 --- a/002_source/cms/src/components/dashboard/vocabulary/vocabularies-pagination.tsx +++ b/002_source/cms/src/components/dashboard/vocabulary/vocabularies-pagination.tsx @@ -6,9 +6,10 @@ import * as React from 'react'; import TablePagination from '@mui/material/TablePagination'; -function noop(): void { - return undefined; -} +// TODO: remove noop +// function noop(): void { +// return undefined; +// } interface VocabulariesPaginationProps { count: number; @@ -29,11 +30,11 @@ export function VocabulariesPagination({ }: VocabulariesPaginationProps): React.JSX.Element { // You should implement the pagination using a similar logic as the filters. // Note that when page change, you should keep the filter search params. - const handleChangePage = (event: unknown, newPage: number) => { + const handleChangePage = (event: unknown, newPage: number): void => { setPage(newPage); }; - const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + const handleChangeRowsPerPage = (event: React.ChangeEvent): void => { setRowsPerPage(parseInt(event.target.value)); // console.log(parseInt(event.target.value)); }; diff --git a/002_source/cms/src/components/dashboard/vocabulary/vocabularies-table.tsx b/002_source/cms/src/components/dashboard/vocabulary/vocabularies-table.tsx index 44ac46d..9161d28 100644 --- a/002_source/cms/src/components/dashboard/vocabulary/vocabularies-table.tsx +++ b/002_source/cms/src/components/dashboard/vocabulary/vocabularies-table.tsx @@ -16,12 +16,13 @@ import { useTranslation } from 'react-i18next'; import { paths } from '@/paths'; import { dayjs } from '@/lib/dayjs'; +import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts'; import { DataTable } from '@/components/core/data-table'; import type { ColumnDef } from '@/components/core/data-table'; import ConfirmDeleteModal from './confirm-delete-modal'; -import { useVocabulariesSelection } from './vocabularies-selection-context'; import type { Vocabulary } from './type'; +import { useVocabulariesSelection } from './vocabularies-selection-context'; function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] { return [ @@ -45,7 +46,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef @@ -104,7 +105,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef { setShowLoading(true); diff --git a/002_source/cms/src/components/dashboard/vocabulary/vocabulary-edit-form.tsx b/002_source/cms/src/components/dashboard/vocabulary/vocabulary-edit-form.tsx index f67992b..fafb941 100644 --- a/002_source/cms/src/components/dashboard/vocabulary/vocabulary-edit-form.tsx +++ b/002_source/cms/src/components/dashboard/vocabulary/vocabulary-edit-form.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; import RouterLink from 'next/link'; import { useParams, useRouter } from 'next/navigation'; import { COL_VOCABULARIES } from '@/constants'; +import { listLessonCategories } from '@/db/LessonCategories/ListLessonCategories'; import { zodResolver } from '@hookform/resolvers/zod'; import { LoadingButton } from '@mui/lab'; import { Avatar, Divider } from '@mui/material'; @@ -12,7 +13,6 @@ import Button from '@mui/material/Button'; import Card from '@mui/material/Card'; import CardActions from '@mui/material/CardActions'; import CardContent from '@mui/material/CardContent'; - import FormControl from '@mui/material/FormControl'; import FormHelperText from '@mui/material/FormHelperText'; import InputLabel from '@mui/material/InputLabel'; @@ -27,8 +27,10 @@ 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 getImageUrlFromFile from '@/lib/get-image-url-from-file.ts'; import { pb } from '@/lib/pb'; import { Option } from '@/components/core/option'; import { toast } from '@/components/core/toaster'; @@ -36,36 +38,6 @@ import FormLoading from '@/components/loading'; import ErrorDisplay from '../error'; import type { EditFormProps } from './type'; -import { listLessonCategories } from '@/db/LessonCategories/listLessonCategories'; -import isDevelopment from '@/lib/check-is-development'; - -const schema = zod.object({ - image: zod.union([zod.array(zod.any()), zod.string()]).optional(), - sound: zod.union([zod.array(zod.any()), zod.string()]).optional(), - word: zod.string().min(1, 'Word is required').max(255), - word_c: zod.string().min(1, 'Chinese word is required').max(255), - sample_e: zod.string().optional(), - sample_c: zod.string().optional(), - cat_id: zod.string().min(1, 'Category ID is required'), - category: zod.string().optional(), - lesson_type_id: zod.string().optional(), - // NOTE: for image handling - avatar: zod.string().optional(), -}); - -type Values = zod.infer; - -const defaultValues = { - image: undefined, - sound: undefined, - word: '', - word_c: '', - sample_e: '', - sample_c: '', - cat_id: '', - category: '', - lesson_type_id: '', -} satisfies Values; export function VocabularyEditForm(): React.JSX.Element { const router = useRouter(); @@ -78,6 +50,36 @@ export function VocabularyEditForm(): React.JSX.Element { // const [showError, setShowError] = React.useState({ show: false, detail: '' }); + const schema = zod.object({ + image: zod.union([zod.array(zod.any()), zod.string()]).optional(), + sound: zod.union([zod.array(zod.any()), zod.string()]).optional(), + word: zod.string().min(1, 'Word is required').max(255), + word_c: zod.string().min(1, 'Chinese word is required').max(255), + sample_e: zod.string().optional(), + sample_c: zod.string().optional(), + cat_id: zod.string().min(1, 'Category ID is required'), + category: zod.string().optional(), + lesson_type_id: zod.string().optional(), + // NOTE: for image handling + avatar: zod.string().optional(), + visible: zod.string(), + }); + + type Values = zod.infer; + + const defaultValues = { + image: undefined, + sound: undefined, + word: '', + word_c: '', + sample_e: '', + sample_c: '', + cat_id: '', + category: '', + lesson_type_id: '', + visible: 'visible', + } satisfies Values; + const { control, handleSubmit, @@ -144,9 +146,7 @@ export function VocabularyEditForm(): React.JSX.Element { reset({ ...defaultValues, ...result }); if (result.image !== '') { - const fetchResult = await fetch( - `http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.image}` - ); + const fetchResult = await fetch(getImageUrlFromFile(result.collectionId, result.id, result.image)); const blob = await fetchResult.blob(); const url = await fileToBase64(blob); diff --git a/002_source/cms/src/components/loading/index.tsx b/002_source/cms/src/components/loading/index.tsx index 219a67e..8d5d23d 100644 --- a/002_source/cms/src/components/loading/index.tsx +++ b/002_source/cms/src/components/loading/index.tsx @@ -19,15 +19,16 @@ function Loading(): React.JSX.Element { alignItems: 'center', }} > - +
{t('loading')}
); } export default function FormLoading(): React.JSX.Element { - const { t } = useTranslation(); - return ( ({ ...prev, user: null, error: 'Something went wrong', isLoading: false })); + setState((prev) => ({ + ...prev, + user: null, + error: `Something went wrong ${isDevelopment ? JSON.stringify({ error }) : ''}`, + isLoading: false, + })); return; } diff --git a/002_source/cms/src/db/Customers/GetActiveCount.tsx b/002_source/cms/src/db/Customers/GetActiveCount.tsx index 4a2f04d..4bb9c99 100644 --- a/002_source/cms/src/db/Customers/GetActiveCount.tsx +++ b/002_source/cms/src/db/Customers/GetActiveCount.tsx @@ -1,4 +1,5 @@ -import { COL_CUSTOMERS, COL_USER_METAS } from '@/constants'; +import { COL_USER_METAS } from '@/constants'; + import { pb } from '@/lib/pb'; export default async function GetActiveCount(): Promise { diff --git a/002_source/cms/src/db/Customers/GetAll.tsx b/002_source/cms/src/db/Customers/GetAll.tsx index 1130771..6d63b06 100644 --- a/002_source/cms/src/db/Customers/GetAll.tsx +++ b/002_source/cms/src/db/Customers/GetAll.tsx @@ -1,6 +1,6 @@ import { pb } from '@/lib/pb'; import { COL_CUSTOMERS } from '@/constants'; -import { RecordModel } from 'pocketbase'; +import type { RecordModel } from 'pocketbase'; export async function getAllCustomers(options = {}): Promise { return pb.collection(COL_CUSTOMERS).getFullList(options); diff --git a/002_source/cms/src/db/Customers/GetBlockedCount.tsx b/002_source/cms/src/db/Customers/GetBlockedCount.tsx index 62e649d..57c537d 100644 --- a/002_source/cms/src/db/Customers/GetBlockedCount.tsx +++ b/002_source/cms/src/db/Customers/GetBlockedCount.tsx @@ -1,4 +1,5 @@ -import { COL_CUSTOMERS, COL_USER_METAS } from '@/constants'; +import { COL_USER_METAS } from '@/constants'; + import { pb } from '@/lib/pb'; export default async function GetBlockedCount(): Promise { diff --git a/002_source/cms/src/db/Customers/GetById.tsx b/002_source/cms/src/db/Customers/GetById.tsx index 7c8ef98..731480b 100644 --- a/002_source/cms/src/db/Customers/GetById.tsx +++ b/002_source/cms/src/db/Customers/GetById.tsx @@ -1,6 +1,6 @@ import { pb } from '@/lib/pb'; import { COL_CUSTOMERS } from '@/constants'; -import { RecordModel } from 'pocketbase'; +import type { RecordModel } from 'pocketbase'; export async function getCustomerById(id: string): Promise { return pb.collection(COL_CUSTOMERS).getOne(id); diff --git a/002_source/cms/src/db/Customers/GetPendingCount.tsx b/002_source/cms/src/db/Customers/GetPendingCount.tsx index eff76fc..b56183c 100644 --- a/002_source/cms/src/db/Customers/GetPendingCount.tsx +++ b/002_source/cms/src/db/Customers/GetPendingCount.tsx @@ -1,4 +1,5 @@ -import { COL_CUSTOMERS, COL_USER_METAS } from '@/constants'; +import { COL_USER_METAS } from '@/constants'; + import { pb } from '@/lib/pb'; export default async function GetPendingCount(): Promise { diff --git a/002_source/cms/src/db/Customers/Helloworld.tsx b/002_source/cms/src/db/Customers/Helloworld.tsx index 2487997..a8e5889 100644 --- a/002_source/cms/src/db/Customers/Helloworld.tsx +++ b/002_source/cms/src/db/Customers/Helloworld.tsx @@ -1,3 +1,3 @@ -export function helloCustomer() { +export function helloCustomer(): string { return 'Hello from Customers module!'; } diff --git a/002_source/cms/src/db/LessonCategories/listLessonCategories.tsx b/002_source/cms/src/db/LessonCategories/ListLessonCategories.tsx similarity index 100% rename from 002_source/cms/src/db/LessonCategories/listLessonCategories.tsx rename to 002_source/cms/src/db/LessonCategories/ListLessonCategories.tsx diff --git a/002_source/cms/src/db/Notifications/GetAll.tsx b/002_source/cms/src/db/Notifications/GetAll.tsx index f43121c..b869865 100644 --- a/002_source/cms/src/db/Notifications/GetAll.tsx +++ b/002_source/cms/src/db/Notifications/GetAll.tsx @@ -3,7 +3,7 @@ // TBA import { pb } from '@/lib/pb'; import { COL_NOTIFICATIONS } from '@/constants'; -import { RecordModel } from 'pocketbase'; +import type { RecordModel } from 'pocketbase'; export async function getAllNotifications(options = {}): Promise { return pb.collection(COL_NOTIFICATIONS).getFullList(options); diff --git a/002_source/cms/src/db/Notifications/GetById.tsx b/002_source/cms/src/db/Notifications/GetById.tsx index ed3f66b..ffae20b 100644 --- a/002_source/cms/src/db/Notifications/GetById.tsx +++ b/002_source/cms/src/db/Notifications/GetById.tsx @@ -3,7 +3,7 @@ // TBA import { pb } from '@/lib/pb'; import { COL_NOTIFICATIONS } from '@/constants'; -import { RecordModel } from 'pocketbase'; +import type { RecordModel } from 'pocketbase'; export async function getNotificationById(id: string): Promise { return pb.collection(COL_NOTIFICATIONS).getOne(id); diff --git a/002_source/cms/src/db/Notifications/Helloworld.tsx b/002_source/cms/src/db/Notifications/Helloworld.tsx index 2487997..a8e5889 100644 --- a/002_source/cms/src/db/Notifications/Helloworld.tsx +++ b/002_source/cms/src/db/Notifications/Helloworld.tsx @@ -1,3 +1,3 @@ -export function helloCustomer() { +export function helloCustomer(): string { return 'Hello from Customers module!'; } diff --git a/002_source/cms/src/db/Notifications/mark-one-as-read.tsx b/002_source/cms/src/db/Notifications/mark-one-as-read.tsx index 828f6f3..e8591f7 100644 --- a/002_source/cms/src/db/Notifications/mark-one-as-read.tsx +++ b/002_source/cms/src/db/Notifications/mark-one-as-read.tsx @@ -1,10 +1,10 @@ // api method for update notification record // RULES: // TBA -import { pb } from '@/lib/pb'; import { COL_NOTIFICATIONS } from '@/constants'; import type { RecordModel } from 'pocketbase'; -import type { NotificationFormProps } from '@/components/dashboard/notification/type.d'; + +import { pb } from '@/lib/pb'; export async function MarkOneAsRead(id: string): Promise { return pb.collection(COL_NOTIFICATIONS).update(id, { read: true }); diff --git a/002_source/cms/src/db/QuizCRCategories/Create.tsx b/002_source/cms/src/db/QuizCRCategories/Create.tsx index 186299f..aa7121c 100644 --- a/002_source/cms/src/db/QuizCRCategories/Create.tsx +++ b/002_source/cms/src/db/QuizCRCategories/Create.tsx @@ -2,7 +2,7 @@ import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; import type { RecordModel } from 'pocketbase'; import { pb } from '@/lib/pb'; -import { CreateFormProps } from '@/components/dashboard/cr/categories/type'; +import type { CreateFormProps } from '@/components/dashboard/cr/categories/type'; export default function createQuizCRCategory(data: CreateFormProps): Promise { return pb.collection(COL_QUIZ_CR_CATEGORIES).create(data); diff --git a/002_source/cms/src/db/QuizLPCategories/Create.tsx b/002_source/cms/src/db/QuizLPCategories/Create.tsx index 5afbb3f..e4f6ab0 100644 --- a/002_source/cms/src/db/QuizLPCategories/Create.tsx +++ b/002_source/cms/src/db/QuizLPCategories/Create.tsx @@ -2,7 +2,7 @@ import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; import type { RecordModel } from 'pocketbase'; import { pb } from '@/lib/pb'; -import { CreateFormProps } from '@/components/dashboard/lp/categories/type'; +import type { CreateFormProps } from '@/components/dashboard/lp/categories/type'; export default function createQuizLPCategory(data: CreateFormProps): Promise { return pb.collection(COL_QUIZ_LP_CATEGORIES).create(data); diff --git a/002_source/cms/src/db/Students/Create.tsx b/002_source/cms/src/db/Students/Create.tsx index d9aa24f..3a46b3c 100644 --- a/002_source/cms/src/db/Students/Create.tsx +++ b/002_source/cms/src/db/Students/Create.tsx @@ -1,12 +1,12 @@ // api method for crate student record // RULES: // TBA -import { COL_STUDENTS, COL_USER_METAS } from '@/constants'; +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 { - return pb.collection(COL_USER_METAS).create(data); + return pb.collection(COL_USER_METAS).create({ ...data, role: 'student' }); } diff --git a/002_source/cms/src/db/Students/Delete.tsx b/002_source/cms/src/db/Students/Delete.tsx index 9f29ed9..e3d42a6 100644 --- a/002_source/cms/src/db/Students/Delete.tsx +++ b/002_source/cms/src/db/Students/Delete.tsx @@ -1,5 +1,6 @@ +import { COL_USER_METAS } from '@/constants'; + import { pb } from '@/lib/pb'; -import { COL_STUDENTS, COL_USER_METAS } from '@/constants'; export async function deleteStudent(id: string): Promise { return pb.collection(COL_USER_METAS).delete(id); diff --git a/002_source/cms/src/db/Students/GetActiveCount.tsx b/002_source/cms/src/db/Students/GetActiveCount.tsx index 58eb51b..3be4a14 100644 --- a/002_source/cms/src/db/Students/GetActiveCount.tsx +++ b/002_source/cms/src/db/Students/GetActiveCount.tsx @@ -1,4 +1,5 @@ -import { COL_STUDENTS, COL_USER_METAS } from '@/constants'; +import { COL_USER_METAS } from '@/constants'; + import { pb } from '@/lib/pb'; export default async function GetActiveCount(): Promise { diff --git a/002_source/cms/src/db/Students/GetAll.tsx b/002_source/cms/src/db/Students/GetAll.tsx index 1e7c2c1..b6b23fc 100644 --- a/002_source/cms/src/db/Students/GetAll.tsx +++ b/002_source/cms/src/db/Students/GetAll.tsx @@ -1,6 +1,6 @@ import { pb } from '@/lib/pb'; import { COL_STUDENTS } from '@/constants'; -import { RecordModel } from 'pocketbase'; +import type { RecordModel } from 'pocketbase'; export async function getAllStudents(options = {}): Promise { return pb.collection(COL_STUDENTS).getFullList(options); diff --git a/002_source/cms/src/db/Students/GetById.tsx b/002_source/cms/src/db/Students/GetById.tsx index 1c0cd17..7fda666 100644 --- a/002_source/cms/src/db/Students/GetById.tsx +++ b/002_source/cms/src/db/Students/GetById.tsx @@ -1,12 +1,16 @@ +// src/db/Students/GetById.tsx +// import { COL_USER_METAS } from '@/constants'; import { pb } from '@/lib/pb'; -import type { DBUserMeta, UserMeta } from '@/components/dashboard/user_meta/type.d'; +import type { DBStudent, Student } from '@/components/dashboard/student/type'; -export async function getStudentById(id: string): Promise { - const record = await pb.collection(COL_USER_METAS).getOne(id, { expand: 'billingAddress, helloworld' }); +export async function getStudentById(id: string): Promise { + const record = await pb + .collection(COL_USER_METAS) + .getOne(id, { expand: 'billingAddress, helloworld', requestKey: null }); - const temp: UserMeta = { + const temp: Student = { id: record.id, name: record.name, email: record.email, diff --git a/002_source/cms/src/db/Students/Helloworld.tsx b/002_source/cms/src/db/Students/Helloworld.tsx index 2487997..c6b9ffa 100644 --- a/002_source/cms/src/db/Students/Helloworld.tsx +++ b/002_source/cms/src/db/Students/Helloworld.tsx @@ -1,3 +1,7 @@ -export function helloCustomer() { +// src/db/Students/Helloworld.tsx +// RULES: +// T.B.A. +// +export function helloCustomer(): string { return 'Hello from Customers module!'; } diff --git a/002_source/cms/src/db/Students/type.d.ts b/002_source/cms/src/db/Students/type.d.ts index 32ed267..2eed10a 100644 --- a/002_source/cms/src/db/Students/type.d.ts +++ b/002_source/cms/src/db/Students/type.d.ts @@ -1,15 +1,71 @@ -import type { BillingAddress } from '@/components/dashboard/user_meta/type.d'; +// 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'; + 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; + + // 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 { @@ -34,6 +90,7 @@ export interface UpdateStudent { timezone?: string; language?: string; currency?: string; + // taxId?: string; } diff --git a/002_source/cms/src/db/Teachers/Create.tsx b/002_source/cms/src/db/Teachers/Create.tsx index 836aac6..04fd5d0 100644 --- a/002_source/cms/src/db/Teachers/Create.tsx +++ b/002_source/cms/src/db/Teachers/Create.tsx @@ -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 { - return pb.collection(COL_TEACHERS).create(data); + return pb.collection(COL_USER_METAS).create({ ...data, role: 'teacher' }); } diff --git a/002_source/cms/src/db/Teachers/GetActiveCount.tsx b/002_source/cms/src/db/Teachers/GetActiveCount.tsx index bb5c779..d43a207 100644 --- a/002_source/cms/src/db/Teachers/GetActiveCount.tsx +++ b/002_source/cms/src/db/Teachers/GetActiveCount.tsx @@ -1,4 +1,5 @@ -import { COL_TEACHERS, COL_USER_METAS } from '@/constants'; +import { COL_USER_METAS } from '@/constants'; + import { pb } from '@/lib/pb'; export default async function GetActiveCount(): Promise { diff --git a/002_source/cms/src/db/Teachers/GetAll.tsx b/002_source/cms/src/db/Teachers/GetAll.tsx index 323a5c0..f37b099 100644 --- a/002_source/cms/src/db/Teachers/GetAll.tsx +++ b/002_source/cms/src/db/Teachers/GetAll.tsx @@ -1,6 +1,7 @@ -import { pb } from '@/lib/pb'; import { COL_TEACHERS } from '@/constants'; -import { RecordModel } from 'pocketbase'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; export async function getAllTeachers(options = {}): Promise { return pb.collection(COL_TEACHERS).getFullList(options); diff --git a/002_source/cms/src/db/Teachers/GetById.tsx b/002_source/cms/src/db/Teachers/GetById.tsx index db16c4d..560d429 100644 --- a/002_source/cms/src/db/Teachers/GetById.tsx +++ b/002_source/cms/src/db/Teachers/GetById.tsx @@ -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 { - 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 { + const record = await pb.collection(COL_USER_METAS).getOne(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; } diff --git a/002_source/cms/src/db/Teachers/Helloworld.tsx b/002_source/cms/src/db/Teachers/Helloworld.tsx index 2487997..a8e5889 100644 --- a/002_source/cms/src/db/Teachers/Helloworld.tsx +++ b/002_source/cms/src/db/Teachers/Helloworld.tsx @@ -1,3 +1,3 @@ -export function helloCustomer() { +export function helloCustomer(): string { return 'Hello from Customers module!'; } diff --git a/002_source/cms/src/db/Teachers/UpdateById.tsx b/002_source/cms/src/db/Teachers/UpdateById.tsx new file mode 100644 index 0000000..9cd61ab --- /dev/null +++ b/002_source/cms/src/db/Teachers/UpdateById.tsx @@ -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): Promise { + return pb.collection(COL_USER_METAS).update(id, data); +} diff --git a/002_source/cms/src/db/Teachers/type.d.ts b/002_source/cms/src/db/Teachers/type.d.ts new file mode 100644 index 0000000..deb4fa2 --- /dev/null +++ b/002_source/cms/src/db/Teachers/type.d.ts @@ -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; + + // status is obsoleted, replace by state + // status: 'pending' | 'active' | 'blocked'; + state?: 'pending' | 'active' | 'blocked'; + // + timezone?: string; + language?: string; + currency?: string; + // + taxId?: string; +} diff --git a/002_source/cms/src/db/UserMetas/Create.tsx b/002_source/cms/src/db/UserMetas/Create.tsx index 6bb4ba0..0b2ebea 100644 --- a/002_source/cms/src/db/UserMetas/Create.tsx +++ b/002_source/cms/src/db/UserMetas/Create.tsx @@ -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 { return pb.collection(COL_USER_METAS).create(data); } diff --git a/002_source/cms/src/db/UserMetas/GetAll.tsx b/002_source/cms/src/db/UserMetas/GetAll.tsx index 4dd4c4c..673b9a1 100644 --- a/002_source/cms/src/db/UserMetas/GetAll.tsx +++ b/002_source/cms/src/db/UserMetas/GetAll.tsx @@ -1,6 +1,6 @@ import { pb } from '@/lib/pb'; import { COL_USER_METAS } from '@/constants'; -import { RecordModel } from 'pocketbase'; +import type { RecordModel } from 'pocketbase'; export async function getAllUserMetas(options = {}): Promise { return pb.collection(COL_USER_METAS).getFullList(options); diff --git a/002_source/cms/src/db/UserMetas/GetById.tsx b/002_source/cms/src/db/UserMetas/GetById.tsx index 04193af..c93d543 100644 --- a/002_source/cms/src/db/UserMetas/GetById.tsx +++ b/002_source/cms/src/db/UserMetas/GetById.tsx @@ -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 { - 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 { + const record = await pb + .collection(COL_USER_METAS) + .getOne(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; } diff --git a/002_source/cms/src/db/UserMetas/Helloworld.tsx b/002_source/cms/src/db/UserMetas/Helloworld.tsx index 2487997..a8e5889 100644 --- a/002_source/cms/src/db/UserMetas/Helloworld.tsx +++ b/002_source/cms/src/db/UserMetas/Helloworld.tsx @@ -1,3 +1,3 @@ -export function helloCustomer() { +export function helloCustomer(): string { return 'Hello from Customers module!'; } diff --git a/002_source/cms/src/db/UserMetas/type.d.ts b/002_source/cms/src/db/UserMetas/type.d.ts index 65c5dd8..a7d6692 100644 --- a/002_source/cms/src/db/UserMetas/type.d.ts +++ b/002_source/cms/src/db/UserMetas/type.d.ts @@ -1,5 +1,38 @@ +// 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; diff --git a/002_source/cms/src/db/Users.old/GetAllCount.tsx b/002_source/cms/src/db/Users.old/GetAllCount.tsx deleted file mode 100644 index b82dc88..0000000 --- a/002_source/cms/src/db/Users.old/GetAllCount.tsx +++ /dev/null @@ -1,15 +0,0 @@ -// REQ0006 -import { COL_USERS } from '@/constants'; - -import { pb } from '@/lib/pb'; - -export default async function GetAllCount(): Promise { - 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; - } -} diff --git a/002_source/cms/src/db/Users.old/_GUIDELINES.md b/002_source/cms/src/db/Users.old/_GUIDELINES.md deleted file mode 100644 index 17cc639..0000000 --- a/002_source/cms/src/db/Users.old/_GUIDELINES.md +++ /dev/null @@ -1,30 +0,0 @@ -# GUIDELINES - -This folder contains drivers for `User`/`Users` records using PocketBase: - -- create (Create.tsx) -- read (GetById.tsx) -- write (Update.tsx) -- count (GetAllCount.tsx) -- delete (Delete.tsx) -- list (GetAll.tsx) - -the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` - -## Assumption and Requirements - -- assume `pb` is located in `@/lib/pb` -- no need to handle error in this function, i'll handle it in the caller -- type information defined in `@/db/Users/type.d.tsx` - -simple template: - -```typescript -import { pb } from '@/lib/pb'; -import { COL_USERS } from '@/constants'; - -export async function createUser(data: CreateFormProps) { - // ...content - // use direct return of pb.collection (e.g. return pb.collection(xxx)) -} -``` diff --git a/003_test/README.md b/002_source/cms/src/db/Users/ChangeUserState.tsx similarity index 100% rename from 003_test/README.md rename to 002_source/cms/src/db/Users/ChangeUserState.tsx diff --git a/002_source/cms/src/db/Users/GetAll.tsx b/002_source/cms/src/db/Users/GetAll.tsx index 1130771..6d63b06 100644 --- a/002_source/cms/src/db/Users/GetAll.tsx +++ b/002_source/cms/src/db/Users/GetAll.tsx @@ -1,6 +1,6 @@ import { pb } from '@/lib/pb'; import { COL_CUSTOMERS } from '@/constants'; -import { RecordModel } from 'pocketbase'; +import type { RecordModel } from 'pocketbase'; export async function getAllCustomers(options = {}): Promise { return pb.collection(COL_CUSTOMERS).getFullList(options); diff --git a/002_source/cms/src/db/Users/GetById.tsx b/002_source/cms/src/db/Users/GetById.tsx index f313892..aa8c2cf 100644 --- a/002_source/cms/src/db/Users/GetById.tsx +++ b/002_source/cms/src/db/Users/GetById.tsx @@ -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 { - try { - const user = await pb.collection(COL_USERS).getOne(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 { + return pb.collection(COL_USERS).getOne(id); } diff --git a/002_source/cms/src/db/Users/Helloworld.tsx b/002_source/cms/src/db/Users/Helloworld.tsx index efbb76d..0714592 100644 --- a/002_source/cms/src/db/Users/Helloworld.tsx +++ b/002_source/cms/src/db/Users/Helloworld.tsx @@ -1,3 +1,3 @@ -export function helloUser() { +export function helloUser(): string { return 'Hello from Users module!'; } diff --git a/002_source/cms/src/db/Users/UpdateById.tsx b/002_source/cms/src/db/Users/UpdateById.tsx new file mode 100644 index 0000000..53cc3d4 --- /dev/null +++ b/002_source/cms/src/db/Users/UpdateById.tsx @@ -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): Promise { + return pb.collection(COL_USERS).update(id, data); +} diff --git a/002_source/cms/src/db/Users/_GUIDELINES.md b/002_source/cms/src/db/Users/_GUIDELINES.md index 0c42524..b652215 100644 --- a/002_source/cms/src/db/Users/_GUIDELINES.md +++ b/002_source/cms/src/db/Users/_GUIDELINES.md @@ -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)) diff --git a/002_source/cms/src/db/Users/type.d.ts b/002_source/cms/src/db/Users/type.d.ts new file mode 100644 index 0000000..08de926 --- /dev/null +++ b/002_source/cms/src/db/Users/type.d.ts @@ -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; +} diff --git a/002_source/cms/src/db/Vocabularies/Create.tsx b/002_source/cms/src/db/Vocabularies/Create.tsx index 6b475be..58a76e4 100644 --- a/002_source/cms/src/db/Vocabularies/Create.tsx +++ b/002_source/cms/src/db/Vocabularies/Create.tsx @@ -1,8 +1,9 @@ import { COL_VOCABULARIES } from '@/constants'; -import type { Vocabulary, VocabularyCreate } from './type'; import { pb } from '@/lib/pb'; +import type { Vocabulary, VocabularyCreate } from './type'; + export default function createVocabulary(data: VocabularyCreate): Promise { return pb.collection(COL_VOCABULARIES).create(data); } diff --git a/002_source/cms/src/db/Vocabularies/Delete.tsx b/002_source/cms/src/db/Vocabularies/Delete.tsx index 39a001b..8e20b07 100644 --- a/002_source/cms/src/db/Vocabularies/Delete.tsx +++ b/002_source/cms/src/db/Vocabularies/Delete.tsx @@ -1,4 +1,5 @@ import { COL_VOCABULARIES } from '@/constants'; + import { pb } from '@/lib/pb'; export default function deleteVocabulary(id: string): Promise { diff --git a/002_source/cms/src/db/Vocabularies/GetAll.tsx b/002_source/cms/src/db/Vocabularies/GetAll.tsx index f6c35a9..d52fa41 100644 --- a/002_source/cms/src/db/Vocabularies/GetAll.tsx +++ b/002_source/cms/src/db/Vocabularies/GetAll.tsx @@ -1,8 +1,9 @@ import { COL_VOCABULARIES } from '@/constants'; -import type { Vocabularies } from './type'; import { pb } from '@/lib/pb'; +import type { Vocabularies } from './type'; + export default function getAllVocabularies(): Promise { return pb.collection(COL_VOCABULARIES).getFullList(); } diff --git a/002_source/cms/src/db/Vocabularies/GetById.tsx b/002_source/cms/src/db/Vocabularies/GetById.tsx index defcf4d..d4015a1 100644 --- a/002_source/cms/src/db/Vocabularies/GetById.tsx +++ b/002_source/cms/src/db/Vocabularies/GetById.tsx @@ -1,8 +1,9 @@ import { COL_VOCABULARIES } from '@/constants'; -import type { Vocabulary } from './type'; import { pb } from '@/lib/pb'; +import type { Vocabulary } from './type'; + export default function getVocabularyById(id: string): Promise { return pb.collection(COL_VOCABULARIES).getOne(id); } diff --git a/002_source/cms/src/db/Vocabularies/GetHiddenCount.tsx b/002_source/cms/src/db/Vocabularies/GetHiddenCount.tsx index d5abacd..4203026 100644 --- a/002_source/cms/src/db/Vocabularies/GetHiddenCount.tsx +++ b/002_source/cms/src/db/Vocabularies/GetHiddenCount.tsx @@ -1,4 +1,5 @@ import { COL_VOCABULARIES } from '@/constants'; + import { pb } from '@/lib/pb'; export default function getHiddenVocabulariesCount(): Promise { diff --git a/002_source/cms/src/db/Vocabularies/GetVisibleCount.tsx b/002_source/cms/src/db/Vocabularies/GetVisibleCount.tsx index 68d5e6c..a208af1 100644 --- a/002_source/cms/src/db/Vocabularies/GetVisibleCount.tsx +++ b/002_source/cms/src/db/Vocabularies/GetVisibleCount.tsx @@ -1,4 +1,5 @@ import { COL_VOCABULARIES } from '@/constants'; + import { pb } from '@/lib/pb'; export default function getVisibleVocabulariesCount(): Promise { diff --git a/002_source/cms/src/db/Vocabularies/Update.tsx b/002_source/cms/src/db/Vocabularies/Update.tsx index ac961b9..6c1db27 100644 --- a/002_source/cms/src/db/Vocabularies/Update.tsx +++ b/002_source/cms/src/db/Vocabularies/Update.tsx @@ -1,6 +1,8 @@ import { COL_VOCABULARIES } from '@/constants'; import type { RecordModel } from 'pocketbase'; + import { pb } from '@/lib/pb'; + import type { EditFormProps } from './type'; export default function updateVocabulary(id: string, data: EditFormProps): Promise { diff --git a/002_source/cms/src/db/Vocabularies/type.d.tsx b/002_source/cms/src/db/Vocabularies/type.d.ts similarity index 100% rename from 002_source/cms/src/db/Vocabularies/type.d.tsx rename to 002_source/cms/src/db/Vocabularies/type.d.ts diff --git a/002_source/cms/src/db/billingAddress/Delete.tsx b/002_source/cms/src/db/billingAddress/Delete.tsx index 9f29ed9..e3d42a6 100644 --- a/002_source/cms/src/db/billingAddress/Delete.tsx +++ b/002_source/cms/src/db/billingAddress/Delete.tsx @@ -1,5 +1,6 @@ +import { COL_USER_METAS } from '@/constants'; + import { pb } from '@/lib/pb'; -import { COL_STUDENTS, COL_USER_METAS } from '@/constants'; export async function deleteStudent(id: string): Promise { return pb.collection(COL_USER_METAS).delete(id); diff --git a/002_source/cms/src/db/billingAddress/GetActiveCount.tsx b/002_source/cms/src/db/billingAddress/GetActiveCount.tsx index 58eb51b..3be4a14 100644 --- a/002_source/cms/src/db/billingAddress/GetActiveCount.tsx +++ b/002_source/cms/src/db/billingAddress/GetActiveCount.tsx @@ -1,4 +1,5 @@ -import { COL_STUDENTS, COL_USER_METAS } from '@/constants'; +import { COL_USER_METAS } from '@/constants'; + import { pb } from '@/lib/pb'; export default async function GetActiveCount(): Promise { diff --git a/002_source/cms/src/db/billingAddress/GetAll.tsx b/002_source/cms/src/db/billingAddress/GetAll.tsx index 1e7c2c1..b6b23fc 100644 --- a/002_source/cms/src/db/billingAddress/GetAll.tsx +++ b/002_source/cms/src/db/billingAddress/GetAll.tsx @@ -1,6 +1,6 @@ import { pb } from '@/lib/pb'; import { COL_STUDENTS } from '@/constants'; -import { RecordModel } from 'pocketbase'; +import type { RecordModel } from 'pocketbase'; export async function getAllStudents(options = {}): Promise { return pb.collection(COL_STUDENTS).getFullList(options); diff --git a/002_source/cms/src/db/billingAddress/GetById.tsx b/002_source/cms/src/db/billingAddress/GetById.tsx index 0fc5f79..34eb4dd 100644 --- a/002_source/cms/src/db/billingAddress/GetById.tsx +++ b/002_source/cms/src/db/billingAddress/GetById.tsx @@ -1,7 +1,11 @@ -import { COL_BILLING_ADDRESS, COL_STUDENTS, COL_USER_METAS } from '@/constants'; -import { RecordModel } from 'pocketbase'; +// src/db/billingAddress/GetById.tsx +// +// PURPOSE: +// to get billing address by its id +// + +import { COL_BILLING_ADDRESS } from '@/constants'; -import { logger } from '@/lib/default-logger'; import { pb } from '@/lib/pb'; import type { DBUserMeta, UserMeta } from '@/components/dashboard/user_meta/type.d'; @@ -10,8 +14,6 @@ export async function getBillingAddressById(id: string): Promise { .collection(COL_BILLING_ADDRESS) .getOne(id, { expand: 'billingAddress, helloworld' }); - console.log({ record }); - const temp: UserMeta = { id: record.id, name: record.name, diff --git a/002_source/cms/src/db/billingAddress/Helloworld.tsx b/002_source/cms/src/db/billingAddress/Helloworld.tsx index 2487997..a8e5889 100644 --- a/002_source/cms/src/db/billingAddress/Helloworld.tsx +++ b/002_source/cms/src/db/billingAddress/Helloworld.tsx @@ -1,3 +1,3 @@ -export function helloCustomer() { +export function helloCustomer(): string { return 'Hello from Customers module!'; } diff --git a/002_source/cms/src/hooks/use-helloworld.ts b/002_source/cms/src/hooks/use-helloworld.ts index f453736..56ceddb 100644 --- a/002_source/cms/src/hooks/use-helloworld.ts +++ b/002_source/cms/src/hooks/use-helloworld.ts @@ -19,6 +19,7 @@ export function useHelloworld(): DialogController { }, []); React.useEffect(() => { + // eslint-disable-next-line no-console console.log('helloworld from useHelloworld'); }, []); diff --git a/002_source/cms/src/lib/auth/custom/client.ts b/002_source/cms/src/lib/auth/custom/client.ts index 70d7554..6f22116 100644 --- a/002_source/cms/src/lib/auth/custom/client.ts +++ b/002_source/cms/src/lib/auth/custom/client.ts @@ -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; @@ -27,7 +22,7 @@ export interface SignUpParams { } export interface SignInWithOAuthParams { - provider: 'google' | 'discord'; + provider: 'google' | 'discord' | 'github'; } export interface SignInWithPasswordParams { diff --git a/002_source/cms/src/lib/check-is-development.ts b/002_source/cms/src/lib/check-is-development.ts index 0da912c..ecb1f12 100644 --- a/002_source/cms/src/lib/check-is-development.ts +++ b/002_source/cms/src/lib/check-is-development.ts @@ -1,2 +1,2 @@ -const isDevelopment = process.env.NEXT_PUBLIC_ENVIRONMENT === 'development'; +const isDevelopment: boolean = process.env.NEXT_PUBLIC_ENVIRONMENT === 'development'; export default isDevelopment; diff --git a/002_source/cms/src/lib/file-to-base64.tsx b/002_source/cms/src/lib/file-to-base64.tsx index 5acdf73..40acef5 100644 --- a/002_source/cms/src/lib/file-to-base64.tsx +++ b/002_source/cms/src/lib/file-to-base64.tsx @@ -15,7 +15,7 @@ export function base64ToFile(base64String: string, filename?: string): Promise { const arr = base64String.split(','); // eslint-disable-next-line prefer-named-capture-group - const type = arr[0].match(/:(.*?);/)![1]; + const type = /:(.*?);/.exec(arr[0])![1]; const bstr = atob(arr[1]); let n = bstr.length; const u8arr = new Uint8Array(n); diff --git a/002_source/cms/src/lib/get-image-url-from-file.ts.ts b/002_source/cms/src/lib/get-image-url-from-file.ts.ts index 68f41a5..5ca47a1 100644 --- a/002_source/cms/src/lib/get-image-url-from-file.ts.ts +++ b/002_source/cms/src/lib/get-image-url-from-file.ts.ts @@ -1,3 +1,10 @@ -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 +// + +import { POCKETBASE_URL } from './pb'; + +export default function getImageUrlFromFile(collectionId: string, id: string, imgFile: string | undefined): string { + return `${POCKETBASE_URL}/api/files/${collectionId}/${id}/${imgFile}`; } diff --git a/002_source/cms/src/lib/helloworld.ts b/002_source/cms/src/lib/helloworld.ts index d9287ed..fc07fe7 100644 --- a/002_source/cms/src/lib/helloworld.ts +++ b/002_source/cms/src/lib/helloworld.ts @@ -1,3 +1,7 @@ +// src/lib/helloworld.ts +// RULES: +// T.B.A. +// export function helloworld(): string { return 'Helloworld'; } diff --git a/002_source/cms/src/lib/pb.ts b/002_source/cms/src/lib/pb.ts index d564382..a0646f2 100644 --- a/002_source/cms/src/lib/pb.ts +++ b/002_source/cms/src/lib/pb.ts @@ -1,3 +1,6 @@ import PocketBase from 'pocketbase'; +if (!process.env.NEXT_PUBLIC_POCKETBASE_URL) throw new Error('the pocketbase url cannot empty'); +export const POCKETBASE_URL: string = process.env.NEXT_PUBLIC_POCKETBASE_URL; + export const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL); diff --git a/002_source/cms/src/paths.ts b/002_source/cms/src/paths.ts index 1c085c5..199530a 100644 --- a/002_source/cms/src/paths.ts +++ b/002_source/cms/src/paths.ts @@ -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`, diff --git a/002_source/cms/tsconfig.json b/002_source/cms/tsconfig.json index 99c0b2e..5701beb 100644 --- a/002_source/cms/tsconfig.json +++ b/002_source/cms/tsconfig.json @@ -5,6 +5,7 @@ "dom", "dom.iterable", "esnext" + // ], "allowJs": true, "skipLibCheck": true, @@ -27,6 +28,7 @@ "paths": { "@/*": [ "./src/*" + // ] } }, @@ -35,6 +37,7 @@ "**/*.ts", "**/*.tsx", ".next/types/**/*.ts" + // ], "exclude": [ "node_modules", @@ -49,6 +52,6 @@ "**/*.draft", "**/*.log", "**/*.tmp", - "**/*del", + "**/*del" ] } diff --git a/002_source/docker/docker-compose.db.yml b/002_source/docker/docker-compose.db.yml index 9a89a7a..4883a58 100644 --- a/002_source/docker/docker-compose.db.yml +++ b/002_source/docker/docker-compose.db.yml @@ -1,3 +1,6 @@ +# related to ./scripts/dc_dev.sh +# consider we are starting in project/002_source/docker, see dc_dev.sh + volumes: shared: dist: @@ -16,10 +19,14 @@ services: ports: - 8090:8090 volumes: + # group custom persistant data inside docker directory - ./volumes/pocketbase/pb_data:/pb_data # + # group dev, seed, schemas into pocketbase dirctory - ../pocketbase/pb_migrations:/pb_migrations - ../pocketbase/pb_hooks:/pb_hooks + + # TODO: resume healthcheck in prod # healthcheck: # #optional (recommended) since v0.10.0 # test: wget --no-verbose --tries=1 --spider http://localhost:8090/api/health || exit 1 @@ -30,6 +37,6 @@ services: deploy: resources: limits: - cpus: 0.5 + cpus: 0.1 reservations: cpus: 0.01 diff --git a/002_source/ionic_mobile/.env.development b/002_source/ionic_mobile/.env.development new file mode 100644 index 0000000..d1a9c2d --- /dev/null +++ b/002_source/ionic_mobile/.env.development @@ -0,0 +1,4 @@ +# +# POCKETBASE running in wsl2 +# +VITE_POCKETBASE_URL=http://192.168.222.199:8090 diff --git a/002_source/ionic_mobile/.env.example b/002_source/ionic_mobile/.env.example new file mode 100644 index 0000000..15b9569 --- /dev/null +++ b/002_source/ionic_mobile/.env.example @@ -0,0 +1,3 @@ +# consider running ionic on the host +# consider pocketbase is running in docker and export port 8090 to the host +VITE_POCKETBASE_URL=http://192.168.222.199:8090 diff --git a/002_source/ionic_mobile/.gitignore b/002_source/ionic_mobile/.gitignore index e428692..4c3d9a8 100644 --- a/002_source/ionic_mobile/.gitignore +++ b/002_source/ionic_mobile/.gitignore @@ -1,6 +1,7 @@ .env **/*.log **/*.del +**/*.draft **/*.bak **/*del diff --git a/002_source/ionic_mobile/TODO.md b/002_source/ionic_mobile/TODO.md index 703cd4c..267e430 100644 --- a/002_source/ionic_mobile/TODO.md +++ b/002_source/ionic_mobile/TODO.md @@ -1,3 +1,4 @@ # TODO - [ ] add login mechanism +- [ ] add task server handle callback tasks diff --git a/002_source/ionic_mobile/default.code-workspace b/002_source/ionic_mobile/ionic_mobile.code-workspace similarity index 100% rename from 002_source/ionic_mobile/default.code-workspace rename to 002_source/ionic_mobile/ionic_mobile.code-workspace diff --git a/002_source/ionic_mobile/package-lock.json b/002_source/ionic_mobile/package-lock.json index 649ed42..aaf700d 100644 --- a/002_source/ionic_mobile/package-lock.json +++ b/002_source/ionic_mobile/package-lock.json @@ -28,14 +28,15 @@ "@types/react-router": "^5.1.20", "@types/react-router-dom": "^5.3.3", "axios": "^1.8.1", - "i18next": "^24.2.2", + "i18next": "^24.2.0", "ionicons": "^7.0.0", "lodash": "^4.17.21", "pocketbase": "^0.26.0", + "qr-code-styling": "^1.9.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "7.50.1", - "react-i18next": "^15.4.1", + "react-i18next": "^15.2.0", "react-markdown": "^9.0.3", "react-router": "^5.3.4", "react-router-dom": "^5.3.4", @@ -50,15 +51,18 @@ "@ianvs/prettier-plugin-sort-imports": "^4.4.1", "@testing-library/dom": ">=7.21.4", "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^14.0.0", + "@testing-library/react": "^16.2.0", "@testing-library/user-event": "^14.4.3", "@types/react": "^18.0.27", "@types/react-dom": "^18.0.10", "@vitejs/plugin-legacy": "^5.0.0", "@vitejs/plugin-react": "^4.0.1", "cypress": "^13.5.0", - "eslint": "^8.35.0", + "eslint": "^9.20.1", "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", "jsdom": "^22.1.0", "prettier": "^3.4.2", "prettier-plugin-organize-imports": "^4.1.0", @@ -66,6 +70,7 @@ "sass": "^1.88.0", "terser": "^5.4.0", "typescript": "^5.1.6", + "typescript-eslint": "^8.24.0", "vite": "~5.2.0", "vitest": "^0.34.6" }, @@ -733,6 +738,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", @@ -1608,6 +1623,16 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/types": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", @@ -2156,9 +2181,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, "license": "MIT", "dependencies": { @@ -2184,17 +2209,55 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -2202,49 +2265,57 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", + "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", "dev": true, "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@hookform/resolvers": { @@ -2256,20 +2327,42 @@ "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", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" }, "engines": { - "node": ">=10.10.0" + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@humanwhocodes/module-importer": { @@ -2286,13 +2379,19 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, - "license": "BSD-3-Clause" + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@hutson/parse-repository-url": { "version": "3.0.2", @@ -2757,6 +2856,48 @@ "integrity": "sha512-QZqem4QuAnAyzfz+Gj5/+SLxqwCAw2qmt7732ZXodr6VDWGeYLG6w1i/vYLa55JQM9wRuBKLmXmiZ2P0LtE5rw==", "license": "MIT" }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.3.tgz", + "integrity": "sha512-rmOWVRUbUJD7iSvJugjUbFZshTAuJ48MXoZ80Osx1GM0K/H1w7rSEvmw8m6vdWxNASgtaHIhAgre4H/E9GJiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/zod": { + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", + "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3297,52 +3438,31 @@ } }, "node_modules/@testing-library/react": { - "version": "14.3.1", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.3.1.tgz", - "integrity": "sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^9.0.0", - "@types/react-dom": "^18.0.0" + "@babel/runtime": "^7.12.5" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@testing-library/react/node_modules/@testing-library/dom": { - "version": "9.3.4", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", - "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@testing-library/react/node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "deep-equal": "^2.0.5" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, "node_modules/@testing-library/user-event": { @@ -3713,6 +3833,13 @@ "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==", "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/lodash": { "version": "4.17.16", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz", @@ -3877,6 +4004,248 @@ "@types/node": "*" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", + "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/type-utils": "8.32.1", + "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", + "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", + "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", + "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/utils": "8.32.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", + "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", + "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", + "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", + "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.1.tgz", @@ -4136,6 +4505,43 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -4787,6 +5193,43 @@ "dev": true, "license": "MIT" }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -4935,6 +5378,16 @@ "dev": true, "license": "MIT" }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -5429,6 +5882,29 @@ "dev": true, "license": "MIT" }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/conventional-changelog": { "version": "3.1.25", "resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-3.1.25.tgz", @@ -6101,6 +6577,26 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/copy-to-clipboard": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", @@ -6143,6 +6639,20 @@ "dev": true, "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -6553,46 +7063,6 @@ "node": ">=6" } }, - "node_modules/deep-equal": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", - "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/deep-equal/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -6688,6 +7158,16 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -6774,19 +7254,6 @@ "node": ">=8" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -6916,6 +7383,13 @@ "safer-buffer": "^2.1.0" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.76", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.76.tgz", @@ -6943,6 +7417,16 @@ "dev": true, "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -7093,34 +7577,6 @@ "node": ">= 0.4" } }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-get-iterator/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, "node_modules/es-iterator-helpers": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", @@ -7254,6 +7710,13 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -7268,60 +7731,66 @@ } }, "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", + "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.13.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.26.0", + "@eslint/plugin-kit": "^0.2.8", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", + "@humanwhocodes/retry": "^0.4.2", + "@modelcontextprotocol/sdk": "^1.8.0", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "zod": "^3.24.2" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-plugin-react": { @@ -7357,6 +7826,29 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, "node_modules/eslint-plugin-react/node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -7389,9 +7881,9 @@ } }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7399,7 +7891,7 @@ "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -7418,48 +7910,55 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, + "license": "Apache-2.0", "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "node_modules/eslint/node_modules/zod": { + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", + "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, + "license": "MIT", "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -7521,6 +8020,16 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/eventemitter2": { "version": "6.4.7", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", @@ -7528,6 +8037,29 @@ "dev": true, "license": "MIT" }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.2.tgz", + "integrity": "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", @@ -7592,6 +8124,104 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -7744,16 +8374,16 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { @@ -7769,6 +8399,24 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -7787,24 +8435,23 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, @@ -7877,6 +8524,26 @@ "url": "https://ko-fi.com/tunnckoCore/commissions" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -8754,13 +9421,16 @@ } }, "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globalthis": { @@ -9127,6 +9797,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -9276,9 +9963,9 @@ "license": "MIT" }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9380,6 +10067,16 @@ "@stencil/core": "^4.0.3" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -9404,23 +10101,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -9764,6 +10444,13 @@ "dev": true, "license": "MIT" }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -11111,6 +11798,16 @@ "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", "license": "CC0-1.0" }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/meow": { "version": "13.2.0", "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", @@ -11124,6 +11821,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -12025,6 +12735,16 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -12213,23 +12933,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -12314,6 +13017,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -12524,6 +13240,16 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -12685,6 +13411,16 @@ "node": ">=0.10.0" } }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-types": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.0.tgz", @@ -12985,6 +13721,20 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-from-env": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", @@ -13038,6 +13788,24 @@ "teleport": ">=0.2.0" } }, + "node_modules/qr-code-styling": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/qr-code-styling/-/qr-code-styling-1.9.2.tgz", + "integrity": "sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg==", + "license": "MIT", + "dependencies": { + "qrcode-generator": "^1.4.4" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/qrcode-generator": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.4.4.tgz", + "integrity": "sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==", + "license": "MIT" + }, "node_modules/qs": { "version": "6.13.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz", @@ -13099,6 +13867,32 @@ "node": ">=8" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -14144,6 +14938,33 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/rrweb-cssom": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", @@ -14363,6 +15184,68 @@ "semver": "bin/semver.js" } }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -14413,6 +15296,13 @@ "node": ">=6.9" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, "node_modules/sharp": { "version": "0.32.6", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", @@ -14884,6 +15774,16 @@ "stacktrace-gps": "^3.0.4" } }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", @@ -14891,20 +15791,6 @@ "dev": true, "license": "MIT" }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/stream-buffers": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", @@ -15335,13 +16221,6 @@ "node": ">=0.10" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, "node_modules/throttle-debounce": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", @@ -15511,6 +16390,16 @@ "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", "license": "MIT" }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tough-cookie": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", @@ -15577,6 +16466,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/ts-easing": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz", @@ -15710,6 +16612,44 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -15802,6 +16742,29 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", + "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.32.1", + "@typescript-eslint/parser": "8.32.1", + "@typescript-eslint/utils": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/ufo": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", @@ -16015,6 +16978,16 @@ "node": ">= 10.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -16118,6 +17091,16 @@ "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==", "license": "MIT" }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", diff --git a/002_source/ionic_mobile/package.json b/002_source/ionic_mobile/package.json index 6c1bf8c..9df6e2c 100644 --- a/002_source/ionic_mobile/package.json +++ b/002_source/ionic_mobile/package.json @@ -43,14 +43,15 @@ "@types/react-router": "^5.1.20", "@types/react-router-dom": "^5.3.3", "axios": "^1.8.1", - "i18next": "^24.2.2", + "i18next": "^24.2.0", "ionicons": "^7.0.0", "lodash": "^4.17.21", "pocketbase": "^0.26.0", + "qr-code-styling": "^1.9.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "7.50.1", - "react-i18next": "^15.4.1", + "react-i18next": "^15.2.0", "react-markdown": "^9.0.3", "react-router": "^5.3.4", "react-router-dom": "^5.3.4", @@ -65,15 +66,18 @@ "@ianvs/prettier-plugin-sort-imports": "^4.4.1", "@testing-library/dom": ">=7.21.4", "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^14.0.0", + "@testing-library/react": "^16.2.0", "@testing-library/user-event": "^14.4.3", "@types/react": "^18.0.27", "@types/react-dom": "^18.0.10", "@vitejs/plugin-legacy": "^5.0.0", "@vitejs/plugin-react": "^4.0.1", "cypress": "^13.5.0", - "eslint": "^8.35.0", + "eslint": "^9.20.1", "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", "jsdom": "^22.1.0", "prettier": "^3.4.2", "prettier-plugin-organize-imports": "^4.1.0", @@ -81,6 +85,7 @@ "sass": "^1.88.0", "terser": "^5.4.0", "typescript": "^5.1.6", + "typescript-eslint": "^8.24.0", "vite": "~5.2.0", "vitest": "^0.34.6" }, diff --git a/002_source/ionic_mobile/scripts/001_build.sh b/002_source/ionic_mobile/scripts/001_build.sh new file mode 100755 index 0000000..aca5dfd --- /dev/null +++ b/002_source/ionic_mobile/scripts/001_build.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -ex + +npm run lint +npx nodemon --ext ts,tsx --exec "npm run build" diff --git a/002_source/ionic_mobile/src/App.tsx b/002_source/ionic_mobile/src/App.tsx index b31c66b..4617510 100644 --- a/002_source/ionic_mobile/src/App.tsx +++ b/002_source/ionic_mobile/src/App.tsx @@ -1,4 +1,4 @@ -import { DEBUG, DEBUG_LINK, LESSON_LINK, QUIZ_MAIN_MENU_LINK, RECORD_LINK, SETTING_LINK } from './constants'; +import { DEBUG_LINK, isDevelop, QUIZ_MAIN_MENU_LINK, RECORD_LINK, SETTING_LINK } from './constants'; /* Core CSS required for Ionic components to work properly */ import '@ionic/react/css/core.css'; @@ -105,8 +105,8 @@ const TabButtons: React.FC = () => { goSwitchPage(LESSON_LINK)} - style={{ color: tab_active == LESSON_LINK ? active_color : inactive_color }} + onClick={() => goSwitchPage(Paths.LESSON_LINK)} + style={{ color: tab_active == Paths.LESSON_LINK ? active_color : inactive_color }} > diff --git a/002_source/ionic_mobile/src/ERRORS.ts b/002_source/ionic_mobile/src/ERRORS.ts new file mode 100644 index 0000000..33f8d33 --- /dev/null +++ b/002_source/ionic_mobile/src/ERRORS.ts @@ -0,0 +1 @@ +export const ERR_POCKETBASE_URL_IS_EMPTY = 'POCKETBASE url is empty'; diff --git a/002_source/ionic_mobile/src/Paths.tsx b/002_source/ionic_mobile/src/Paths.tsx index a50a8c0..1b0d071 100644 --- a/002_source/ionic_mobile/src/Paths.tsx +++ b/002_source/ionic_mobile/src/Paths.tsx @@ -1,9 +1,18 @@ const Paths = { - AuthHome: `/auth/Home`, + LESSON_LINK: '/lesson', + // + 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 }; diff --git a/002_source/ionic_mobile/src/RouteConfig.tsx b/002_source/ionic_mobile/src/RouteConfig.tsx index 2438661..254ad84 100644 --- a/002_source/ionic_mobile/src/RouteConfig.tsx +++ b/002_source/ionic_mobile/src/RouteConfig.tsx @@ -3,7 +3,6 @@ import { CONNECTIVE_REVISION_LINK, DEBUG_LINK, FAVORITE_LINK, - LESSON_LINK, LESSON_WORD_PAGE_LINK, LISTENING_PRACTICE_LINK, MATCHING_FRENZY_LINK, @@ -28,7 +27,7 @@ import { AuthSignUp } from './pages/auth/SignUp'; import Lesson from './pages/Lesson/index'; // NOTES: old version using json file -import LessonWordPageByDb from './pages/Lesson/LessonWordPageByDb'; +// import LessonWordPageByDb from './pages/Lesson/LessonWordPageByDb'; import WordPage from './pages/Lesson/WordPage'; // import ListeningPractice from './pages/ListeningPractice'; @@ -45,14 +44,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 './pages/auth/AuthorizedTest/auth-guard'; +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() { @@ -67,11 +69,11 @@ function RouteConfig() { {/* */} - + {/* */} - + {/* */} @@ -180,6 +182,25 @@ function RouteConfig() { + {/* protected page */} + + + + + + + + + + + + + + + + + + @@ -196,18 +217,12 @@ function RouteConfig() { - + - - - - - - ); } diff --git a/002_source/ionic_mobile/src/components/CustomField/index.tsx b/002_source/ionic_mobile/src/components/CustomField/index.tsx index 0032083..c974f6b 100644 --- a/002_source/ionic_mobile/src/components/CustomField/index.tsx +++ b/002_source/ionic_mobile/src/components/CustomField/index.tsx @@ -7,7 +7,23 @@ interface CustomFieldProps { label: string; required: boolean; input: { - props: { type: string; placeholder: string }; + props: { + type: + | 'date' + | 'email' + | 'number' + | 'password' + | 'search' + | 'tel' + | 'text' + | 'url' + | 'time' + | 'week' + | 'month' + | 'datetime-local'; + placeholder: string; + // + }; state: { value: string; reset: (newValue: any) => void; @@ -20,8 +36,8 @@ interface CustomFieldProps { } 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; + const error = errors && errors.filter((e: { id: string }) => e.id === field.id)[0]; + const errorMessage = error && errors.filter((e: { id: string }) => e.id === field.id)[0].message; return ( <> @@ -30,7 +46,12 @@ function CustomField({ field, errors }: CustomFieldProps): React.JSX.Element { {field.label} {error &&

{errorMessage}

} - + ); diff --git a/002_source/ionic_mobile/src/components/Footer/index.tsx b/002_source/ionic_mobile/src/components/Footer/index.tsx new file mode 100644 index 0000000..b9b4f6b --- /dev/null +++ b/002_source/ionic_mobile/src/components/Footer/index.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +function Footer(): React.JSX.Element { + return ( +
+ 2025 louislabs +
+ ); +} + +export default Footer; diff --git a/002_source/ionic_mobile/src/components/LoadingScreen/index.tsx b/002_source/ionic_mobile/src/components/LoadingScreen/index.tsx index 53aa3c2..e4aa145 100644 --- a/002_source/ionic_mobile/src/components/LoadingScreen/index.tsx +++ b/002_source/ionic_mobile/src/components/LoadingScreen/index.tsx @@ -1,25 +1,32 @@ import { IonContent, IonPage, IonSpinner } from '@ionic/react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; -export function LoadingScreen() { - const { t, i18n } = useTranslation(); +export function LoadingSpinner(): React.JSX.Element { + const { t } = useTranslation(); + return ( +
+ +
{t('Loading')}
+
+ ); +} + +export function LoadingScreen(): React.JSX.Element { return ( -
- -
{t('Loading')}
-
+
); diff --git a/002_source/ionic_mobile/src/components/SettingContainer/index.tsx b/002_source/ionic_mobile/src/components/SettingContainer/index.tsx index cfe16d0..38a66c0 100644 --- a/002_source/ionic_mobile/src/components/SettingContainer/index.tsx +++ b/002_source/ionic_mobile/src/components/SettingContainer/index.tsx @@ -1,8 +1,9 @@ import { IonButton, IonIcon, useIonRouter } from '@ionic/react'; import { arrowBack } from 'ionicons/icons'; -import { LESSON_LINK, VERSIONS } from '../../constants'; +import { VERSIONS } from '../../constants'; import SettingSvg from './image.svg'; import { Paths } from '../../Paths'; +import { pb } from '../../lib/pb'; interface ContainerProps { name: string; @@ -15,6 +16,14 @@ const SettingContainer: React.FC = ({ name }) => { 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 (
= ({ name }) => {

T.B.A.

{VERSIONS}
- AuthHome + User Profile { - router.push(LESSON_LINK, undefined, 'replace'); + router.push(Paths.LESSON_LINK, undefined, 'replace'); }} style={{ marginTop: '1rem' }} > diff --git a/002_source/ionic_mobile/src/pages/auth/AuthorizedTest/auth-guard.tsx b/002_source/ionic_mobile/src/components/auth/auth-guard.tsx similarity index 93% rename from 002_source/ionic_mobile/src/pages/auth/AuthorizedTest/auth-guard.tsx rename to 002_source/ionic_mobile/src/components/auth/auth-guard.tsx index 3b3beec..385e0a2 100644 --- a/002_source/ionic_mobile/src/pages/auth/AuthorizedTest/auth-guard.tsx +++ b/002_source/ionic_mobile/src/components/auth/auth-guard.tsx @@ -1,8 +1,8 @@ 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'; +import { useUser } from '../../hooks/use-user'; +import { Paths } from '../../Paths'; export interface AuthGuardProps { children: React.ReactNode; diff --git a/002_source/ionic_mobile/src/constants.tsx b/002_source/ionic_mobile/src/constants.tsx index 80a9d44..18c3c88 100644 --- a/002_source/ionic_mobile/src/constants.tsx +++ b/002_source/ionic_mobile/src/constants.tsx @@ -8,6 +8,10 @@ // 0.0.7 - add back button for listening practice, matching frenzy, connective revision // 0.0.8 - add back button for listening practice card, matching frenzy card, connective revision card // 0.0.9 - fix ticket 26,27,28,29 +import { Capacitor } from '@capacitor/core'; + +import { ERR_POCKETBASE_URL_IS_EMPTY } from './ERRORS'; + // 0.0.10 - remove debug symbol and re-route ending page const VERSIONS = 'v0.0.10'; const HELLOWORLD_MP3 = '/helloworld.mp3'; @@ -21,7 +25,8 @@ const MATCHING_FRENZY_LINK = '/matching_frenzy'; const FAVORITE_LINK = '/fav'; const LESSON_WORD_PAGE_LINK = '/lesson_word_page'; const CONNECTIVE_REVISION_LINK = '/connective_revision'; -const LESSON_LINK = '/lesson'; +// TODO: remove me +// const LESSON_LINK_mdel = '/lesson'; const QUIZ_MAIN_MENU_LINK = '/quizzes_main_menu'; const RECORD_LINK = '/record'; const SETTING_LINK = '/setting'; @@ -76,11 +81,16 @@ const TEST = process.env.NODE_ENV === 'test'; const MY_FAVORITE = 'My Favorite'; // +if (!import.meta.env.VITE_POCKETBASE_URL) throw new Error(ERR_POCKETBASE_URL_IS_EMPTY); const POCKETBASE_URL = import.meta.env.VITE_POCKETBASE_URL; // // database constants export const COL_USERS = 'users'; export const COL_USER_METAS = 'UserMetas'; +// +export const RUNNING_PLATFORM = Capacitor.getPlatform(); + +export const isDevelop = import.meta.env.DEV; export { // @@ -112,7 +122,6 @@ export { // HELLOWORLD, HELLOWORLD_MP3, - LESSON_LINK, LESSON_WORD_PAGE_LINK, LISTENING_PRACTICE_LINK, LISTENING_PRACTICE_TIME_SPENT, diff --git a/002_source/ionic_mobile/src/contexts/AppState.tsx b/002_source/ionic_mobile/src/contexts/AppState.tsx index 03e399f..a3f8e9e 100644 --- a/002_source/ionic_mobile/src/contexts/AppState.tsx +++ b/002_source/ionic_mobile/src/contexts/AppState.tsx @@ -1,14 +1,14 @@ import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react'; import RemoveFavoritePrompt from '../components/RemoveFavoritePrompt'; -import { LESSON_LINK } from '../constants'; +import { Paths } from '../Paths'; const AppStateContext = createContext(undefined); export const AppStateProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const [my_context, setMyContext] = useState('initial value'); - const [tab_active, setTabActive] = useState(LESSON_LINK); + const [tab_active, setTabActive] = useState(Paths.LESSON_LINK); const [show_confirm_user_exit, useShowConfirmUserExit] = useState(false); - const [url_push_after_user_confirm, setURLPushAfterUserConfirm] = useState(LESSON_LINK); + const [url_push_after_user_confirm, setURLPushAfterUserConfirm] = useState(Paths.LESSON_LINK); const [matching_frenzy_in_progress, setMatchingFrenzyInProgress] = useState(false); const [connective_revision_in_progress, setConnectiveRevisionInProgress] = useState(false); diff --git a/002_source/ionic_mobile/src/contexts/CheckScreenWidth/QrHere/avatar.jpg b/002_source/ionic_mobile/src/contexts/CheckScreenWidth/QrHere/avatar.jpg new file mode 100644 index 0000000..5e08e63 Binary files /dev/null and b/002_source/ionic_mobile/src/contexts/CheckScreenWidth/QrHere/avatar.jpg differ diff --git a/002_source/ionic_mobile/src/contexts/CheckScreenWidth/QrHere/index.tsx b/002_source/ionic_mobile/src/contexts/CheckScreenWidth/QrHere/index.tsx new file mode 100644 index 0000000..ba97a2f --- /dev/null +++ b/002_source/ionic_mobile/src/contexts/CheckScreenWidth/QrHere/index.tsx @@ -0,0 +1,51 @@ +import { useEffect, useRef, useState } from 'react'; +import './styles.css'; +import QRCodeStyling from 'qr-code-styling'; +import AvatarJpg from './avatar.jpg'; + +const qrCode = new QRCodeStyling({ + width: 200, + height: 200, + image: AvatarJpg, + dotsOptions: { + color: '#4267b2', + type: 'rounded', + }, + imageOptions: { + crossOrigin: 'anonymous', + margin: 2, + }, +}); + +export default function App() { + const [url, setUrl] = useState(window.location.href); + const [fileExt, setFileExt] = useState('png'); + const ref = useRef(null); + + useEffect(() => { + if (ref.current) { + qrCode.append(ref.current); + } + }, []); + + useEffect(() => { + qrCode.update({ + data: url, + }); + }, [url]); + + const onUrlChange = (event: React.ChangeEvent) => { + event.preventDefault(); + setUrl(event.target.value); + }; + + const onExtensionChange = (event: React.ChangeEvent) => { + setFileExt(event.target.value); + }; + + return ( +
+
+
+ ); +} diff --git a/002_source/ionic_mobile/src/contexts/CheckScreenWidth/QrHere/styles.css b/002_source/ionic_mobile/src/contexts/CheckScreenWidth/QrHere/styles.css new file mode 100644 index 0000000..59b0604 --- /dev/null +++ b/002_source/ionic_mobile/src/contexts/CheckScreenWidth/QrHere/styles.css @@ -0,0 +1,4 @@ +.App { + font-family: sans-serif; + text-align: center; +} diff --git a/002_source/ionic_mobile/src/contexts/CheckScreenWidth/index.tsx b/002_source/ionic_mobile/src/contexts/CheckScreenWidth/index.tsx new file mode 100644 index 0000000..852368b --- /dev/null +++ b/002_source/ionic_mobile/src/contexts/CheckScreenWidth/index.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import QrHere from './QrHere'; +import { IonText } from '@ionic/react'; +import { useTranslation } from 'react-i18next'; +import Footer from '../../components/Footer'; + +export interface ProviderProps { + children: React.ReactNode; +} + +export function CheckScreenWidth({ children }: ProviderProps): React.JSX.Element { + const { t } = useTranslation(); + const [showQrCode, setShowQrCode] = React.useState(false); + React.useEffect(() => { + setShowQrCode(window.screen.width > 800); + }, []); + return ( + <> + {showQrCode ? ( + <> +
+ + + {t('please-use-mobile-for-better-experience')} +
+
+ + ) : ( + <>{children} + )} + + ); +} diff --git a/002_source/ionic_mobile/src/contexts/I18nProvider.tsx b/002_source/ionic_mobile/src/contexts/I18nProvider.tsx new file mode 100644 index 0000000..bbff96c --- /dev/null +++ b/002_source/ionic_mobile/src/contexts/I18nProvider.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; + +import '../i18n'; + +export interface I18nProviderProps { + language?: string; +} + +export function I18nProvider({ language = 'en' }: I18nProviderProps): React.JSX.Element { + const { i18n } = useTranslation(); + + React.useEffect(() => { + // + }, [i18n, language]); + + return <>; +} diff --git a/002_source/ionic_mobile/src/contexts/index.tsx b/002_source/ionic_mobile/src/contexts/index.tsx index 4626b2a..5a2e685 100644 --- a/002_source/ionic_mobile/src/contexts/index.tsx +++ b/002_source/ionic_mobile/src/contexts/index.tsx @@ -1,6 +1,8 @@ import { PocketBaseProvider } from '../hooks/usePocketBase'; import { AppStateProvider } from './AppState'; import { UserProvider } from './auth/user-context'; +import { CheckScreenWidth } from './CheckScreenWidth'; +import { I18nProvider } from './I18nProvider'; import { MyIonFavoriteProvider } from './MyIonFavorite'; import { MyIonMetricProvider } from './MyIonMetric'; import { MyIonQuizProvider } from './MyIonQuiz'; @@ -12,24 +14,27 @@ const queryClient = new QueryClient(); const ContextMeta = ({ children }: { children: React.ReactNode }) => { return ( <> - - - - - - - - - {children} - {/* */} - - - - - - - - + + + + + + + + + + + {children} + {/* */} + + + + + + + + + ); }; diff --git a/002_source/ionic_mobile/src/data/utils.tsx b/002_source/ionic_mobile/src/data/utils.tsx index 0388ed6..4d14215 100644 --- a/002_source/ionic_mobile/src/data/utils.tsx +++ b/002_source/ionic_mobile/src/data/utils.tsx @@ -3,23 +3,23 @@ import { useState } from 'react'; export const useFormInput = (initialValue = '') => { const [value, setValue] = useState(initialValue); - const handleChange = async (e) => { + const handleChange = async (e: React.ChangeEvent) => { const tempValue = await e.currentTarget.value; setValue(tempValue); }; return { value, - reset: (newValue) => setValue(newValue), + reset: (newValue: string) => setValue(newValue), onIonChange: handleChange, onKeyUp: handleChange, }; }; -export const validateForm = (fields) => { - let errors = []; +export const validateForm = (fields: { required: boolean; id: string; input: { state: { value: string } } }[]) => { + let errors: { id: string; message: string }[] = []; - fields.forEach((field) => { + fields.forEach((field: { required: boolean; id: string; input: { state: { value: string } } }) => { if (field.required) { const fieldValue = field.input.state.value; diff --git a/002_source/ionic_mobile/src/db/UserMetas/GetById.tsx b/002_source/ionic_mobile/src/db/UserMetas/GetById.tsx index bbcd5af..e7ea16e 100644 --- a/002_source/ionic_mobile/src/db/UserMetas/GetById.tsx +++ b/002_source/ionic_mobile/src/db/UserMetas/GetById.tsx @@ -4,5 +4,8 @@ import { COL_USER_METAS } from '../../constants'; import { pb } from '../../lib/pb'; export async function getUserMetaById(id: string): Promise { - return pb.collection(COL_USER_METAS).getOne(id); + return pb.collection(COL_USER_METAS).getOne(id, { + expand: 'billingAddress', + requestKey: null, + }); } diff --git a/002_source/ionic_mobile/src/db/UserMetas/type.d.ts b/002_source/ionic_mobile/src/db/UserMetas/type.d.ts index 65c5dd8..b6174bf 100644 --- a/002_source/ionic_mobile/src/db/UserMetas/type.d.ts +++ b/002_source/ionic_mobile/src/db/UserMetas/type.d.ts @@ -1,5 +1,19 @@ 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; diff --git a/002_source/ionic_mobile/src/hooks/fetchMFQuestions.tsx b/002_source/ionic_mobile/src/hooks/fetchMFQuestions.tsx index 29f0507..e667778 100644 --- a/002_source/ionic_mobile/src/hooks/fetchMFQuestions.tsx +++ b/002_source/ionic_mobile/src/hooks/fetchMFQuestions.tsx @@ -1,5 +1,3 @@ -import { QuizMFQuestion } from '../types/QuizMFQuestion'; -import { usePocketBase } from './usePocketBase'; import { QueryClient } from '@tanstack/react-query'; const queryClient = new QueryClient({ @@ -10,12 +8,12 @@ const queryClient = new QueryClient({ }, }); -const fetchMFQuestions = async (cat_id: string, pb: any) => { +async function fetchMFQuestions(cat_id: string, pb: any) { const response = await queryClient.fetchQuery({ queryKey: ['fetchData'], staleTime: 60 * 1000, queryFn: async () => { - return await pb.collection('QuizMFQuestions').getList(1, 9999, { + return await pb.collection('QuizMFQuestions').getList(1, 9999, { filter: `cat_id = "${cat_id}"`, $autoCancel: false, }); @@ -23,6 +21,6 @@ const fetchMFQuestions = async (cat_id: string, pb: any) => { }); return response; -}; +} export default fetchMFQuestions; diff --git a/002_source/ionic_mobile/src/hooks/useGetVocabularyRoute.tsx b/002_source/ionic_mobile/src/hooks/useGetVocabularyRoute.tsx index bb08fe2..61ad056 100644 --- a/002_source/ionic_mobile/src/hooks/useGetVocabularyRoute.tsx +++ b/002_source/ionic_mobile/src/hooks/useGetVocabularyRoute.tsx @@ -1,9 +1,7 @@ -import { usePocketBase } from './usePocketBase.tsx'; -import type LessonsTypes from '../types/LessonsTypes'; +import { usePocketBase } from './usePocketBase'; import { useQuery } from '@tanstack/react-query'; -import Vocabularies from '../types/Vocabularies.tsx'; -const useGetVocabularyRoute = (lessonType: string, catId: string) => { +function useGetVocabularyRoute(lessonType: string, catId: string) { const { user, pb } = usePocketBase(); return useQuery({ queryKey: ['useGetVocabularyRoute', lessonType, catId, 'feeds', 'all', user?.id || ''], @@ -14,7 +12,7 @@ const useGetVocabularyRoute = (lessonType: string, catId: string) => { queryKey: ['useGetVocabularyRoute', string, string, 'feeds', 'all', string | null]; }) => { console.log('calling useGetLessonCategoriesRoute'); - return await pb.collection('Vocabularies').getList(1, 9999, { + return await pb.collection('Vocabularies').getList(1, 9999, { // TODO: sort by field -> pos sort: 'id', filter: `lesson_type_id = "${lessonType}" && cat_id = "${catId}"`, @@ -24,6 +22,6 @@ const useGetVocabularyRoute = (lessonType: string, catId: string) => { }, // enabled: !!user?.id, }); -}; +} export default useGetVocabularyRoute; diff --git a/002_source/ionic_mobile/src/hooks/useListAllLessonTypes.tsx b/002_source/ionic_mobile/src/hooks/useListAllLessonTypes.tsx index 484aa63..cf4298f 100644 --- a/002_source/ionic_mobile/src/hooks/useListAllLessonTypes.tsx +++ b/002_source/ionic_mobile/src/hooks/useListAllLessonTypes.tsx @@ -1,30 +1,14 @@ -import { usePocketBase } from './usePocketBase.tsx'; +import { usePocketBase } from './usePocketBase'; import { useQuery } from '@tanstack/react-query'; import { LessonsType } from '../types/LessonsTypes'; const useListAllLessonTypes = () => { const { user, pb } = usePocketBase(); return useQuery({ - queryKey: [ - 'useListAllLessonTypes', - 'feeds', - 'all', - user?.id || '', - // - ], + queryKey: ['useListAllLessonTypes', 'feeds', 'all', user?.id || ''], staleTime: 60 * 1000, - queryFn: async ({ - queryKey, - }: { - queryKey: [ - 'useListAllLessonTypes', - 'feeds', - 'all', - string | null, - // - ]; - }) => { - console.log('calling useListAllLessonTypes'); + queryFn: async ({ queryKey }: { queryKey: ['useListAllLessonTypes', 'feeds', 'all', string | null] }) => { + // console.log('calling useListAllLessonTypes'); return await pb.collection('LessonsTypes').getFullList({ // TODO: sort by field -> pos sort: 'id', diff --git a/002_source/ionic_mobile/src/hooks/useListQuizCRCategories.tsx b/002_source/ionic_mobile/src/hooks/useListQuizCRCategories.tsx index 3b2d02f..ed14cdd 100644 --- a/002_source/ionic_mobile/src/hooks/useListQuizCRCategories.tsx +++ b/002_source/ionic_mobile/src/hooks/useListQuizCRCategories.tsx @@ -1,7 +1,7 @@ // CR = ConnectiveRevision -import { usePocketBase } from './usePocketBase.tsx'; +import { usePocketBase } from './usePocketBase'; import { useQuery } from '@tanstack/react-query'; -import IListeningPracticeCategory from '../interfaces/IListeningPracticeCategory.tsx'; +import IListeningPracticeCategory from '../interfaces/IListeningPracticeCategory'; const useListQuizCRCategories = () => { const { user, pb } = usePocketBase(); diff --git a/002_source/ionic_mobile/src/hooks/useListQuizCRQuestionByCRCategoryId.tsx b/002_source/ionic_mobile/src/hooks/useListQuizCRQuestionByCRCategoryId.tsx index 41dbb04..663e393 100644 --- a/002_source/ionic_mobile/src/hooks/useListQuizCRQuestionByCRCategoryId.tsx +++ b/002_source/ionic_mobile/src/hooks/useListQuizCRQuestionByCRCategoryId.tsx @@ -1,6 +1,6 @@ -import { usePocketBase } from './usePocketBase.tsx'; +import { usePocketBase } from './usePocketBase'; import { useQuery } from '@tanstack/react-query'; -import { QuizCRQuestion } from '../types/QuizCRQuestion.ts/index.ts'; +import { QuizCRQuestion } from '../types/QuizCRQuestion'; const useListQuizCRQuestionByCRCategoryId = (CRCategoryId: string) => { const { user, pb } = usePocketBase(); diff --git a/002_source/ionic_mobile/src/hooks/useListQuizLPQuestionByLPCategoryId.tsx b/002_source/ionic_mobile/src/hooks/useListQuizLPQuestionByLPCategoryId.tsx index 9afe476..adcffad 100644 --- a/002_source/ionic_mobile/src/hooks/useListQuizLPQuestionByLPCategoryId.tsx +++ b/002_source/ionic_mobile/src/hooks/useListQuizLPQuestionByLPCategoryId.tsx @@ -1,4 +1,4 @@ -import { usePocketBase } from './usePocketBase.tsx'; +import { usePocketBase } from './usePocketBase'; import { useQuery } from '@tanstack/react-query'; // import { QuizLPQuestion } from '../types/QuizLPQuestion'; diff --git a/002_source/ionic_mobile/src/hooks/useListQuizListeningPracticeContent.tsx b/002_source/ionic_mobile/src/hooks/useListQuizListeningPracticeContent.tsx index 77ced27..f390af9 100644 --- a/002_source/ionic_mobile/src/hooks/useListQuizListeningPracticeContent.tsx +++ b/002_source/ionic_mobile/src/hooks/useListQuizListeningPracticeContent.tsx @@ -1,6 +1,6 @@ -import { usePocketBase } from './usePocketBase.tsx'; +import { usePocketBase } from './usePocketBase'; import { useQuery } from '@tanstack/react-query'; -import IListeningPracticeCategory from '../interfaces/IListeningPracticeCategory.tsx'; +import IListeningPracticeCategory from '../interfaces/IListeningPracticeCategory'; const useListQuizListeningPracticeContent = () => { const { user, pb } = usePocketBase(); diff --git a/002_source/ionic_mobile/src/hooks/useListQuizMFQuestionsByCategoryId.tsx b/002_source/ionic_mobile/src/hooks/useListQuizMFQuestionsByCategoryId.tsx index 093819b..65b67e5 100644 --- a/002_source/ionic_mobile/src/hooks/useListQuizMFQuestionsByCategoryId.tsx +++ b/002_source/ionic_mobile/src/hooks/useListQuizMFQuestionsByCategoryId.tsx @@ -1,6 +1,6 @@ import { QuizMFQuestion } from '../types/QuizMFQuestion'; import { usePocketBase } from './usePocketBase'; -import { QueryClient } from '@tanstack/react-query'; +import { QueryClient, useQuery } from '@tanstack/react-query'; const queryClient = new QueryClient({ defaultOptions: { diff --git a/002_source/ionic_mobile/src/hooks/usePocketBase.tsx b/002_source/ionic_mobile/src/hooks/usePocketBase.tsx index 85a48ab..a734432 100644 --- a/002_source/ionic_mobile/src/hooks/usePocketBase.tsx +++ b/002_source/ionic_mobile/src/hooks/usePocketBase.tsx @@ -25,7 +25,12 @@ export const PocketBaseProvider = ({ children }: { children: any }) => { const logout = useCallback(() => pb.authStore.clear(), [pb.authStore]); - return {children}; + return ( + + {/* */} + {children} + + ); }; export const usePocketBase = () => { @@ -36,15 +41,15 @@ export const usePocketBase = () => { return context; }; -export const useRequireAuth = () => { - const { pb, user } = usePocketBase(); - const navigate = useNavigate(); +// export const useRequireAuth = () => { +// const { pb, user } = usePocketBase(); +// const navigate = useNavigate(); - useEffect(() => { - if (!pb.authStore.isValid) { - navigate(URLS.LOGIN); - } - }, [pb.authStore.isValid, navigate]); +// useEffect(() => { +// if (!pb.authStore.isValid) { +// navigate(URLS.LOGIN); +// } +// }, [pb.authStore.isValid, navigate]); - return user; -}; +// return user; +// }; diff --git a/002_source/ionic_mobile/src/i18n.ts b/002_source/ionic_mobile/src/i18n.ts index ed63dab..aafaf69 100644 --- a/002_source/ionic_mobile/src/i18n.ts +++ b/002_source/ionic_mobile/src/i18n.ts @@ -1,107 +1,19 @@ import i18n from 'i18next'; +// import Backend from "i18next-http-backend"; +// import LanguageDetector from "i18next-browser-languagedetector"; import { initReactI18next } from 'react-i18next'; - -// the translations -// (tip move them in a JSON file and import them) -const resources = { - en: { - translation: { - 'Lesson': 'Lesson', - 'Quiz': 'Quiz', - 'Favorite': 'Favorite', - 'Loading': 'Loading', - 'menu.link-home': 'Home', - 'menu.link-stats': 'Stats', - 'home.title': 'Remove duplicate songs from your Spotify library.', - 'home.description': - "Spotify Dedup cleans up your playlists and liked songs from your Spotify account. It's easy and fast.", - 'home.review': - 'Read what {{-supportersCount}} supporters think about Spotify Dedup on {{- linkOpen}}Buy Me a Coffee{{- linkClose}}', - 'home.login-button': 'Log in with Spotify', - 'meta.title': 'Spotify Dedup - Remove duplicate songs from your Spotify library', - 'meta.description': - 'Delete repeated songs from your Spotify playlists and liked songs automatically. Fix your music library. Quickly and easy.', - 'features.find-remove.header': 'Find & remove', - 'features.find-remove.body': - 'Dedup checks your playlists and liked songs in {{- strongOpen}}your Spotify library{{- strongClose}}. Once Dedup finds duplicates you can remove them on a per-playlist basis.', - 'features.safer.header': 'Safer', - 'features.safer.body': - 'Dedup will only remove {{- strongOpen}}duplicate songs{{- strongClose}}, leaving the rest of the playlist and liked songs untouched.', - 'features.open-source.header': 'Open Source', - 'features.open-source.body': - "You might want to have a look at the {{- linkGithubOpen}}source code on GitHub{{- linkGithubClose}}. This web app uses the {{- linkWebApiOpen}}Spotify Web API{{- linkWebApiClose}} to manage user's playlists and liked songs.", - 'reviews.title': 'This is what users are saying', - 'footer.author': 'Made with ♥ by {{- linkOpen}}JMPerez 👨‍💻{{- linkClose}}', - 'footer.github': 'Check out the {{- linkOpen}}code on GitHub 📃{{- linkClose}}', - 'footer.bmc': 'Support the project {{- linkOpen}}buying a coffee ☕{{- linkClose}}', - 'bmc.button': 'Would you buy me a coffee?', - 'result.duplicate.reason-same-id': 'Duplicate', - 'result.duplicate.reason-same-data': 'Duplicate (same name, artist and duration)', - 'result.duplicate.track': '<0>{{trackName}} <2>by <4>{{trackArtistName}}', - 'process.status.finding': 'Finding duplicates in your playlists and liked songs…', - 'process.status.complete': 'Processing complete!', - 'process.status.complete.body': 'Your playlists and liked songs have been processed!', - 'process.status.complete.dups.body': - 'Click on the {{- strongOpen}}Remove duplicates{{- strongClose}} button to get rid of duplicates in that playlist or liked songs collection.', - 'process.status.complete.nodups.body': "Congrats! You don't have duplicates in your playlists nor liked songs.", - 'process.reading-library': 'Going through your library, finding the playlists you own and your liked songs…', - 'process.processing_one': 'Searching for duplicate songs, wait a sec. Still to process {{count}} playlist…', - 'process.processing_other': 'Searching for duplicate songs, wait a sec. Still to process {{count}} playlists…', - 'process.saved.title': 'liked songs in your library', - 'process.saved.duplicates_one': 'This collection has {{count}} duplicate song', - 'process.saved.duplicates_other': 'This collection has {{count}} duplicate songs', - 'process.saved.remove-button': 'Remove duplicates from your liked songs', - 'process.playlist.duplicates_one': 'This playlist has {{count}} duplicate song', - 'process.playlist.duplicates_other': 'This playlist has {{count}} duplicate songs', - 'process.playlist.remove-button': 'Remove duplicates from this playlist', - 'process.items.removed': 'Duplicates removed', - 'faq.section-title': 'Frequently asked questions', - 'faq.question-1': 'What does this web application do?', - 'faq.answer-1': - 'Spotify Dedup helps you clean up your music libraries on Spotify by identifying and deleting duplicate songs across playlists and liked songs.', - 'faq.question-2': 'How does it find duplicates?', - 'faq.answer-2': - "Dedup finds duplicates based on the songs identifier, title, artist, and duration similarity. It identifies duplicates that Spotify's application does not catch.", - 'faq.question-3': "How is Dedup better than Spotify's built-in duplicate detection?", - 'faq.answer-3': - "Spotify's applications only warn about duplicates when adding a song to a playlit or liked songs with the exact same song identifier. However, the same song can have multiple identifiers on Spotify that both in the same release or in several ones. Dedup detects duplicates based on title, artist, and duration similarity.", - 'faq.question-4': 'When duplicates are found, which songs are removed?', - 'faq.answer-4': 'Dedup will keep the first song within a group of duplicate songs, and will remove the rest.', - 'faq.question-5': 'Is my data safe with this web application?', - 'faq.answer-5': - 'Yes, this web application does not store any user data on its servers. It only requests the minimum set of permissions necessary to process your library.', - 'faq.question-6': 'What permissions does this web application require?', - 'faq.answer-6': - "This web application uses Spotify's authentication service to access your liked songs and playlists in your library.", - 'faq.question-7': 'How has this tool been tested?', - 'faq.answer-7': - 'This tool has been battle-tested by thousands of users who have used it to identify duplicates in millions of playlists since 2014.', - 'faq.question-8': 'Can this tool delete duplicates across multiple playlists?', - 'faq.answer-8': - "This tool can identify and delete duplicates on all playlists in a library, but doesn't detect duplicates of a song across multiple playlists.", - 'faq.question-9': 'How can I revoke the permissions granted to this web application?', - 'faq.answer-9': - "You can revoke the permissions granted to this web application at any time on your Spotify account, under the 'Apps' section.", - 'faq.question-10': 'Does this tool work with other music streaming services?', - 'faq.answer-10': "No, this tool only works with Spotify through Spotify's Web API.", - }, - }, -}; +import en from './locale/en'; +import zh from './locale/zh'; i18n - .use(initReactI18next) // passes i18n down to react-i18next + // pass the i18n instance to react-i18next. + .use(initReactI18next) + // init i18next + // for all options read: https://www.i18next.com/overview/configuration-options .init({ - // the translations - // (tip move them in a JSON file and import them, - // or even better, manage them via a UI: https://react.i18next.com/guides/multiple-translation-files#manage-your-translations-with-a-management-gui) - resources, - // if you're using a language detector, do not define the lng option - lng: 'en', + resources: { en, zh }, fallbackLng: 'en', - - interpolation: { - escapeValue: false, // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape - }, + // debug: true, }); export default i18n; diff --git a/002_source/ionic_mobile/src/lib/get-image-url-from-file.ts.ts b/002_source/ionic_mobile/src/lib/get-image-url-from-file.ts.ts new file mode 100644 index 0000000..b7d0aa2 --- /dev/null +++ b/002_source/ionic_mobile/src/lib/get-image-url-from-file.ts.ts @@ -0,0 +1,10 @@ +// +// PURPOSE: +// get file url from pocketbase record +// + +import { POCKETBASE_URL } from '../constants'; + +export default function getImageUrlFromFile(collectionId: string, id: string, imgFile: string | undefined): string { + return `${POCKETBASE_URL}/api/files/${collectionId}/${id}/${imgFile}`; +} diff --git a/002_source/ionic_mobile/src/lib/getStudentAvatar.tsx b/002_source/ionic_mobile/src/lib/getStudentAvatar.tsx new file mode 100644 index 0000000..5f8f7f1 --- /dev/null +++ b/002_source/ionic_mobile/src/lib/getStudentAvatar.tsx @@ -0,0 +1,7 @@ +import { POCKETBASE_URL } from '../constants'; +import { DBUserMeta } from '../db/UserMetas/type'; + +export function getStudentAvatarUrl(studentMeta: DBUserMeta) { + const { collectionId, id, avatar } = studentMeta; + return `url(${POCKETBASE_URL}/api/files/${collectionId}/${id}/${avatar})`; +} diff --git a/002_source/ionic_mobile/src/lib/pb.ts b/002_source/ionic_mobile/src/lib/pb.ts index 729e137..d6f25aa 100644 --- a/002_source/ionic_mobile/src/lib/pb.ts +++ b/002_source/ionic_mobile/src/lib/pb.ts @@ -1,5 +1,7 @@ import PocketBase from 'pocketbase'; +import { ERR_POCKETBASE_URL_IS_EMPTY } from '../ERRORS'; +import { POCKETBASE_URL } from '../constants'; -const pb = new PocketBase('http://127.0.0.1:8090'); +const pb = new PocketBase(POCKETBASE_URL); export { pb }; diff --git a/002_source/ionic_mobile/src/locale/en.ts b/002_source/ionic_mobile/src/locale/en.ts new file mode 100644 index 0000000..2f7cbab --- /dev/null +++ b/002_source/ionic_mobile/src/locale/en.ts @@ -0,0 +1,12 @@ +// the translations +// (tip move them in a JSON file and import them) +const en = { + translation: { + 'hello': 'world', + 'Loading': 'loading', + 'lesson': 'lesson', + 'please-use-mobile-for-better-experience': 'Please use mobile for better experience', + }, +}; + +export default en; diff --git a/002_source/ionic_mobile/src/locale/zh.ts b/002_source/ionic_mobile/src/locale/zh.ts new file mode 100644 index 0000000..e504bd4 --- /dev/null +++ b/002_source/ionic_mobile/src/locale/zh.ts @@ -0,0 +1,12 @@ +// the translations +// (tip move them in a JSON file and import them) +const zh = { + translation: { + 'hello': '你好', + 'Loading': '加載中', + 'lesson': '課程', + 'please-use-mobile-for-better-experience': '為了更好的體驗,請使用手機瀏覽 😊', + }, +}; + +export default zh; diff --git a/002_source/ionic_mobile/src/pages/Lesson/ConnectivesPage/index.tsx b/002_source/ionic_mobile/src/pages/Lesson/ConnectivesPage/index.tsx index 8e6bfd7..515afdf 100644 --- a/002_source/ionic_mobile/src/pages/Lesson/ConnectivesPage/index.tsx +++ b/002_source/ionic_mobile/src/pages/Lesson/ConnectivesPage/index.tsx @@ -16,9 +16,7 @@ import Markdown from 'react-markdown'; import { useParams } from 'react-router'; import { useGlobalAudioPlayer } from 'react-use-audio-player'; import remarkGfm from 'remark-gfm'; -import { LoadingScreen } from '../../../components/LoadingScreen'; import RemoveFavoritePrompt from '../../../components/RemoveFavoritePrompt'; -import { LESSON_LINK } from '../../../constants'; import { useMyIonFavorite } from '../../../contexts/MyIonFavorite'; import ILesson from '../../../interfaces/ILesson'; import { listLessonContent } from '../../../public_data/listLessonContent'; diff --git a/002_source/ionic_mobile/src/pages/Lesson/LessonContainer/index.tsx b/002_source/ionic_mobile/src/pages/Lesson/LessonContainer/index.tsx index 5049b62..53ffa93 100644 --- a/002_source/ionic_mobile/src/pages/Lesson/LessonContainer/index.tsx +++ b/002_source/ionic_mobile/src/pages/Lesson/LessonContainer/index.tsx @@ -35,7 +35,7 @@ const LessonContainer: React.FC = ({ lesson_type_id: lesson_type if (loading) return ; if (!selected_content) return ; - if (selected_content.length == 0) return <>loading; + if (selected_content.length == 0) return ; return ( <> @@ -49,32 +49,30 @@ const LessonContainer: React.FC = ({ lesson_type_id: lesson_type }} > {selected_content.map((content: any, cat_idx: number) => ( - <> - { - // TODO: review the layout type `v` and `c` - router.push(`/lesson_word_page/v/${lesson_type_id}/${content.id}/0`, undefined, 'replace'); - }} - > -
-
- {content.cat_name} -
-
- + { + // TODO: review the layout type `v` and `c` + router.push(`/lesson_word_page/v/${lesson_type_id}/${content.id}/0`, undefined, 'replace'); + }} + > +
+
+ {content.cat_name} +
+
))}
{/* */} diff --git a/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/AudioControls.tsx b/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/AudioControls.tsx deleted file mode 100644 index 8fe3e79..0000000 --- a/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/AudioControls.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { FunctionComponent, useEffect } from 'react'; -import { useGlobalAudioPlayer } from 'react-use-audio-player'; -import { useListeningPracticeTimeSpent } from '../../../contexts/MyIonMetric/ListeningPracticeTimeSpent'; - -export const AudioControls: FunctionComponent<{ audio_src: string }> = ({ audio_src }) => { - const { play, pause, playing, duration } = useGlobalAudioPlayer(); - const { load, src: loadedSrc } = useGlobalAudioPlayer(); - let { myIonMetricIncListeningPracticeTimeSpent } = useListeningPracticeTimeSpent(); - - useEffect(() => { - if (audio_src) { - load(audio_src); - } - }, [audio_src]); - - useEffect(() => { - if (loadedSrc) { - } - }, [loadedSrc]); - - useEffect(() => { - if (playing) { - myIonMetricIncListeningPracticeTimeSpent(duration); - } - }, [playing]); - - return <>; -}; diff --git a/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/index.tsx b/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/index.tsx deleted file mode 100644 index a6ca557..0000000 --- a/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/index.tsx +++ /dev/null @@ -1,319 +0,0 @@ -import { IonButton, IonButtons, IonContent, IonIcon, IonModal, IonPage, IonToolbar, useIonRouter } from '@ionic/react'; -import './style.css'; -import { - arrowBackCircleOutline, - chevronBack, - chevronForward, - close, - heart, - heartOutline, - play, - volumeHighOutline, -} from 'ionicons/icons'; -import { useEffect, useRef, useState } from 'react'; -// -import Markdown from 'react-markdown'; -import { useParams } from 'react-router'; -import { useGlobalAudioPlayer } from 'react-use-audio-player'; -import remarkGfm from 'remark-gfm'; -import { LoadingScreen } from '../../../components/LoadingScreen'; -import RemoveFavoritePrompt from '../../../components/RemoveFavoritePrompt'; -import { LESSON_LINK } from '../../../constants'; -import { useMyIonFavorite } from '../../../contexts/MyIonFavorite'; -// -import { useMyIonStore } from '../../../contexts/MyIonStore'; -import ILesson from '../../../interfaces/ILesson'; -import ILessonCategory from '../../../interfaces/ILessonCategory'; -import IWordCard from '../../../interfaces/IWordCard'; -import { getFavLessonVocabularyLink, getLessonVocabularyLink } from '../getLessonWordLink'; -import { AudioControls } from './AudioControls'; - -const LessonWordPageByDb: React.FC = () => { - const [loading, setLoading] = useState(true); - - const router = useIonRouter(); - const modal = useRef(null); - const { lesson_idx, cat_idx, word_idx } = useParams<{ lesson_idx: string; cat_idx: string; word_idx: string }>(); - - const [open_remove_modal, setOpenRemoveModal] = useState(false); - const [lesson_info, setLessonInfo] = useState(undefined); - const [cat_info, setCatInfo] = useState(undefined); - const [word_info, setWordInfo] = useState(undefined); - - // const { play: play_word, playing } = useGlobalAudioPlayer(); - const { myIonStoreAddFavoriteVocabulary, myIonStoreRemoveFavoriteVocabulary, myIonStoreFindInFavoriteVocabulary } = - useMyIonFavorite(); - let [favorite_address, setFavoriteAddress] = useState(getFavLessonVocabularyLink(lesson_idx, cat_idx, word_idx)); - - useEffect(() => { - setFavoriteAddress(getFavLessonVocabularyLink(lesson_idx, cat_idx, word_idx)); - - // if (lesson_contents.length > 0) { - // let lesson_content: ILesson = lesson_contents[parseInt(lesson_idx)]; - // let category_content: ILessonCategory = lesson_content.content[parseInt(cat_idx)]; - // let word_content: IWordCard = category_content.content[parseInt(word_idx)]; - // setWordInfo(word_content); - // } - }, [lesson_idx, cat_idx, word_idx]); - - function dismiss() { - setOpenRemoveModal(false); - } - const [isOpen, setIsOpen] = useState(false); - - let { lesson_contents } = useMyIonStore(); - - useEffect(() => { - // NOTES: lesson_content == [] during loading - if (lesson_contents.length > 0) { - let lesson_content: ILesson = lesson_contents[parseInt(lesson_idx)]; - let category_content: ILessonCategory = lesson_content.content[parseInt(cat_idx)]; - let word_content: IWordCard = category_content.content[parseInt(word_idx)]; - - setLessonInfo(lesson_content); - setCatInfo(category_content); - setWordInfo(word_content); - - setLoading(false); - } - }, [lesson_contents]); - - let [in_fav, setInFav] = useState(false); - const isInFavorite = async (string_to_search: string) => { - let result = await myIonStoreFindInFavoriteVocabulary(string_to_search); - setInFav(result); - }; - - const addToFavorite = async (string_to_add: string) => { - await myIonStoreAddFavoriteVocabulary(string_to_add); - - await isInFavorite(string_to_add); - setInFav(!in_fav); - }; - - function handleUserRemoveFavorite() { - setOpenRemoveModal(true); - } - - const removeFromFavorite = async (string_to_remove: string) => { - await myIonStoreRemoveFavoriteVocabulary(string_to_remove); - await isInFavorite(string_to_remove); - }; - - useEffect(() => { - (async () => { - await isInFavorite(getFavLessonVocabularyLink(lesson_idx, cat_idx, word_idx)); - })(); - }, [lesson_idx, cat_idx, word_idx]); - - return <>should not see me; - - if (lesson_info == undefined) return ; - if (!cat_info || !word_info) return ; - - return ( - <> - - -
- { - router.push(`${LESSON_LINK}/a/${lesson_info.name}`); - }} - > - - -
-
-
{cat_info.cat_name}
-
-
- {/* */} -
-
- { - router.push( - getLessonVocabularyLink(lesson_idx, cat_idx, Math.max(0, parseInt(word_idx) - 1).toString()), - ); - }} - > - - -
-
-
- { - router.push( - getLessonVocabularyLink( - lesson_idx, - cat_idx, - Math.min(cat_info.content.length - 1, parseInt(word_idx) + 1).toString(), - ), - ); - }} - > - - -
-
- -
-
- {parseInt(word_idx) + 1} -
-
- -
-
-
- - (playing ? null : play_word())} - > - - -
-
{word_info.word}
-
- { - in_fav ? handleUserRemoveFavorite() : addToFavorite(favorite_address); - }} - > - - -
-
- -
-
{word_info.word_c}
-
-
- -
- {word_info.sample_e} - {word_info.sample_c} -
-
-
-
- {/* */} - - - - - - dismiss()} shape="round" fill="clear"> - - - - -
-
-
Are you sure to remove favorite ?
- -
-
- dismiss()} fill="outline"> - Cancel - -
-
- { - removeFromFavorite(getFavLessonVocabularyLink(lesson_idx, cat_idx, word_idx)); - setIsOpen(true); - dismiss(); - }} - fill="solid" - color="danger" - > - Remove - -
-
-
-
-
-
- - - - ); -}; - -export default LessonWordPageByDb; diff --git a/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/style.css b/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/style.css deleted file mode 100644 index a76cae3..0000000 --- a/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/style.css +++ /dev/null @@ -1,31 +0,0 @@ -.bold { - font-weight: bold; -} - -ion-modal#example-modal { - --height: 33%; - --width: 80%; - --border-radius: 16px; - --box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); -} - -ion-modal#example-modal::part(backdrop) { - /* background: rgba(209, 213, 219); */ - opacity: 1; -} - -ion-modal#example-modal ion-toolbar { - /* --background: rgb(14 116 144); */ - /* --color: white; */ - --color: black; -} - -ion-toast.custom-toast::part(message) { - text-align: center; - font-size: 1.5rem; - color: rgba(0, 0, 0, 0.9); -} - -ion-toast.custom-toast::part(container) { - bottom: 100px; -} diff --git a/002_source/ionic_mobile/src/pages/Lesson/WordPage/index.tsx b/002_source/ionic_mobile/src/pages/Lesson/WordPage/index.tsx index e2af3e6..75adac7 100644 --- a/002_source/ionic_mobile/src/pages/Lesson/WordPage/index.tsx +++ b/002_source/ionic_mobile/src/pages/Lesson/WordPage/index.tsx @@ -1,11 +1,9 @@ -import { IonButton, IonButtons, IonContent, IonIcon, IonModal, IonPage, IonToolbar, useIonRouter } from '@ionic/react'; +import { IonButton, IonContent, IonIcon, IonPage, useIonRouter } from '@ionic/react'; import './style.css'; import { arrowBackCircleOutline, chevronBack, chevronForward, - close, - heart, heartOutline, play, volumeHighOutline, @@ -17,24 +15,17 @@ import { useParams } from 'react-router'; import { useGlobalAudioPlayer } from 'react-use-audio-player'; import remarkGfm from 'remark-gfm'; import { LoadingScreen } from '../../../components/LoadingScreen'; -import RemoveFavoritePrompt from '../../../components/RemoveFavoritePrompt'; -import { LESSON_LINK } from '../../../constants'; +import { POCKETBASE_URL } from '../../../constants'; import { useMyIonFavorite } from '../../../contexts/MyIonFavorite'; // -import { useMyIonStore } from '../../../contexts/MyIonStore'; import ILesson from '../../../interfaces/ILesson'; import ILessonCategory from '../../../interfaces/ILessonCategory'; import IWordCard from '../../../interfaces/IWordCard'; -import { - getFavLessonVocabularyLink, - getLessonVocabularyLink, - getLessonVocabularyLinkString, -} from '../../Lesson/getLessonWordLink'; +import { getFavLessonVocabularyLink, getLessonVocabularyLinkString } from '../../Lesson/getLessonWordLink'; import { AudioControls } from './AudioControls'; import useGetVocabularyRoute from '../../../hooks/useGetVocabularyRoute'; -import { UseQueryResult } from '@tanstack/react-query'; -import { ListResult } from 'pocketbase'; -import LessonsTypes from '../../../types/LessonsTypes'; +import { Vocabulary } from '../../../types/Vocabularies'; +import { Paths } from '../../../Paths'; const WordPage: React.FC = () => { const [loading, setLoading] = useState(true); @@ -56,7 +47,7 @@ const WordPage: React.FC = () => { let { status, data: tempResult } = useGetVocabularyRoute(lesson_idx, cat_idx); function getFile(recordId: string, fileName: string) { - return `http://127.0.0.1:8090/api/files/Vocabularies/${recordId}/${fileName}`; + return `${POCKETBASE_URL}/api/files/Vocabularies/${recordId}/${fileName}`; } useEffect(() => { @@ -78,12 +69,11 @@ const WordPage: React.FC = () => { let [lastWord, setLastWord] = useState(false); useEffect(() => { - if (tempResult) { + if (tempResult && tempResult.items.length > 0) { + let temp1 = tempResult.items[parseInt(word_idx)] as unknown as Vocabulary; try { - setCatInfo(tempResult.items[parseInt(word_idx)].expand.cat_id as unknown as ILessonCategory); - // - setWordInfo(tempResult.items[parseInt(word_idx)] as unknown as IWordCard); - // + setCatInfo(temp1.expand.cat_id as unknown as ILessonCategory); + setWordInfo(temp1 as unknown as IWordCard); setLastWord(parseInt(word_idx) === tempResult.items.length - 1); } catch (error) { console.error(error); @@ -92,12 +82,11 @@ const WordPage: React.FC = () => { }, [lesson_idx, cat_idx, word_idx]); useEffect(() => { - // console.log({ lesson_idx, cat_idx, word_idx }); - if (tempResult) { + if (tempResult && tempResult.items.length > 0) { + let temp1 = tempResult.items[parseInt(word_idx)] as unknown as Vocabulary; try { - setCatInfo(tempResult.items[parseInt(word_idx)].expand.cat_id as unknown as ILessonCategory); - // - setWordInfo(tempResult.items[parseInt(word_idx)] as unknown as IWordCard); + setCatInfo(temp1.expand.cat_id as unknown as ILessonCategory); + setWordInfo(temp1 as unknown as IWordCard); // setLastWord(parseInt(word_idx) === tempResult.items.length - 1); } catch (error) { @@ -121,7 +110,7 @@ const WordPage: React.FC = () => { fill="clear" color={'dark'} onClick={() => { - router.push(`${LESSON_LINK}/a/Vocabulary`); + router.push(`${Paths.LESSON_LINK}/a/Vocabulary`); }} > @@ -143,11 +132,7 @@ const WordPage: React.FC = () => { // href={getLessonVocabularyLink(lesson_idx, cat_idx, Math.max(0, parseInt(word_idx) - 1).toString())} onClick={() => { router.push( - getLessonVocabularyLinkString( - lesson_idx, - cat_idx, - Math.max(0, parseInt(word_idx) - 1).toString(), - ), + getLessonVocabularyLinkString(lesson_idx, cat_idx, Math.max(0, parseInt(word_idx) - 1).toString()) ); }} > @@ -203,8 +188,8 @@ const WordPage: React.FC = () => { getLessonVocabularyLinkString( lesson_idx, cat_idx, - Math.min(tempResult.items.length - 1, parseInt(word_idx) + 1).toString(), - ), + Math.min(tempResult.items.length - 1, parseInt(word_idx) + 1).toString() + ) ); }} > diff --git a/002_source/ionic_mobile/src/pages/Lesson/index.tsx b/002_source/ionic_mobile/src/pages/Lesson/index.tsx index 309fb14..18e721f 100644 --- a/002_source/ionic_mobile/src/pages/Lesson/index.tsx +++ b/002_source/ionic_mobile/src/pages/Lesson/index.tsx @@ -1,6 +1,9 @@ import { + IonButton, + IonButtons, IonContent, IonHeader, + IonIcon, IonItem, IonList, IonPage, @@ -15,20 +18,18 @@ import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router'; import ExitButton from '../../components/ExitButton'; -import { LoadingScreen } from '../../components/LoadingScreen'; +import { LoadingScreen, LoadingSpinner } from '../../components/LoadingScreen'; import CongratConnectiveConqueror from '../../components/Modal/Congratulation/ConnectiveConqueror'; import CongratGenius from '../../components/Modal/Congratulation/Genius'; import CongratHardworker from '../../components/Modal/Congratulation/Hardworker'; import CongratListeningProgress from '../../components/Modal/Congratulation/ListeningProgress'; import CongratMatchmaking from '../../components/Modal/Congratulation/Matchmaking'; -import { LESSON_LINK } from '../../constants'; -import { useMyIonStore } from '../../contexts/MyIonStore'; -import { listLessonCategories } from '../../public_data/listLessonCategories'; import LessonContainer from './LessonContainer'; -import useHelloworld from '../../hooks/useHelloworld'; import useListAllLessonTypes from '../../hooks/useListAllLessonTypes'; -import LessonsTypes, { LessonsType } from '../../types/LessonsTypes'; -import useListCategoriesByLessonId from '../../hooks/useListCategoriesByLessonId'; +import { LessonsType } from '../../types/LessonsTypes'; +import { ellipsisHorizontal, ellipsisVertical, personCircle, search } from 'ionicons/icons'; +import { Capacitor } from '@capacitor/core'; +import { RUNNING_PLATFORM } from '../../constants'; const Lesson: React.FC = () => { const { act_category } = useParams<{ act_category: string }>(); @@ -60,30 +61,27 @@ const Lesson: React.FC = () => { -
- - {t('Lesson')} - -
+ {/* show when scroll up */} + + {RUNNING_PLATFORM == 'android' ? ( + -
-
+ + ) : ( + <> + )} + + {t('lesson')} {t('hello')} +
+ + {/* show when no scrolling */} -
{'Lesson'}
+
{t('lesson')}
@@ -113,7 +111,7 @@ const Lesson: React.FC = () => { {lessonTypes[active_lesson_idx]?.id ? ( ) : ( - <>loading (id undefined) + )} {/* */} diff --git a/002_source/ionic_mobile/src/pages/Setting/indx.tsx b/002_source/ionic_mobile/src/pages/Setting/index.tsx similarity index 100% rename from 002_source/ionic_mobile/src/pages/Setting/indx.tsx rename to 002_source/ionic_mobile/src/pages/Setting/index.tsx diff --git a/002_source/ionic_mobile/src/pages/auth/AuthorizedTest/index.tsx b/002_source/ionic_mobile/src/pages/auth/AuthorizedTest/index.tsx index f837eac..973899a 100644 --- a/002_source/ionic_mobile/src/pages/auth/AuthorizedTest/index.tsx +++ b/002_source/ionic_mobile/src/pages/auth/AuthorizedTest/index.tsx @@ -22,12 +22,25 @@ import _ from 'lodash'; import { Router, useParams } from 'react-router'; import { Wave } from '../../../components/Wave'; import { Paths } from '../../../Paths'; +import { useTransition } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useUser } from '../../../hooks/use-user'; function AuthorizedTest(): React.JSX.Element { const router = useIonRouter(); + const { t } = useTranslation(); + const { user } = useUser(); + function handleBackToLogin() { router.push(Paths.AuthLogin); } + + function handleViewStudentInfoOnClick() { + if (user?.id) { + router.push(Paths.GetStudentInfoLink(user.id)); + } + } + return ( {/* */} @@ -36,6 +49,12 @@ function AuthorizedTest(): React.JSX.Element { Authorized page test + {JSON.stringify({ user })} + {/* */} + + {t('view-student-info')} + + {/* */} Back to login diff --git a/002_source/ionic_mobile/src/pages/auth/Home/index copy.tsx b/002_source/ionic_mobile/src/pages/auth/Home/index copy.tsx new file mode 100644 index 0000000..2b9e57d --- /dev/null +++ b/002_source/ionic_mobile/src/pages/auth/Home/index copy.tsx @@ -0,0 +1,107 @@ +import { + IonButton, + IonCardTitle, + IonCol, + IonContent, + IonFooter, + IonGrid, + IonHeader, + IonImg, + IonPage, + IonRouterLink, + IonRow, + IonToolbar, + useIonRouter, +} from '@ionic/react'; +// import { Action } from '../components/Action'; +import styles from './style.module.scss'; +import { Action } from '../../../components/Action'; +import { Paths } from '../../../Paths'; +import { useTranslation } from 'react-i18next'; +import { useEffect, useState } from 'react'; +import { useUser } from '../../../hooks/use-user'; +import { LoadingScreen } from '../../../components/LoadingScreen'; + +const AuthHome = () => { + const { t } = useTranslation(); + const { user, checkSession, isLoading } = useUser(); + const router = useIonRouter(); + + const [showLoading, setShowLoading] = useState(true); + const [showError, setShowErrr] = useState<{ show: boolean; message: string }>({ + show: false, + message: '', + }); + + const [checkingSession, setCheckingSession] = useState(true); + + useEffect(() => { + if (!checkingSession) { + if (!user) { + router.push(Paths.AuthLogin); + } else { + router.push(Paths.AuthorizedTest); + } + setShowLoading(false); + } + }, [user, checkingSession]); + + useEffect(() => { + checkSession?.() + .then(() => { + setCheckingSession(false); + }) + .catch((err) => console.error(err)); + }, [checkSession]); + + if (showLoading) return ; + // if (showError) return <>{showError.message}; + + return ( + + + {/* */} + + {/* */} + + +
+ + + + + {/* */} + Join millions of other people discovering their creative side + + + + + + + + + {/* */} + Get started → + + + + + +
+
+ + + + + + +
+ ); +}; + +export default AuthHome; diff --git a/002_source/ionic_mobile/src/pages/auth/Home/index.tsx b/002_source/ionic_mobile/src/pages/auth/Home/index.tsx index 7cca67d..3ee1795 100644 --- a/002_source/ionic_mobile/src/pages/auth/Home/index.tsx +++ b/002_source/ionic_mobile/src/pages/auth/Home/index.tsx @@ -11,16 +11,33 @@ import { IonRouterLink, IonRow, IonToolbar, + useIonRouter, } from '@ionic/react'; // import { Action } from '../components/Action'; import styles from './style.module.scss'; import { Action } from '../../../components/Action'; import { Paths } from '../../../Paths'; import { useTranslation } from 'react-i18next'; +import { useEffect, useState } from 'react'; +import { useUser } from '../../../hooks/use-user'; +import { LoadingScreen } from '../../../components/LoadingScreen'; const AuthHome = () => { const { t } = useTranslation(); + const [showLoading, setShowLoading] = useState(true); + const [showError, setShowErrr] = useState<{ show: boolean; message: string }>({ + show: false, + message: '', + }); + + useEffect(() => { + setShowLoading(false); + }, []); + + if (showLoading) return ; + if (showError.show) return <>{showError.message}; + return ( diff --git a/002_source/ionic_mobile/src/pages/auth/Login/index.tsx b/002_source/ionic_mobile/src/pages/auth/Login/index.tsx index 4a8d07f..fa374d3 100644 --- a/002_source/ionic_mobile/src/pages/auth/Login/index.tsx +++ b/002_source/ionic_mobile/src/pages/auth/Login/index.tsx @@ -63,8 +63,8 @@ function AuthLogin(): React.JSX.Element { type Values = zod.infer; const defaultValues = { - email: 'user5@123.com', - password: 'user5@123.com', + email: '', + password: '', // } satisfies Values; @@ -81,10 +81,13 @@ function AuthLogin(): React.JSX.Element { const onSubmit = React.useCallback( async (values: Values): Promise => { - // + console.log({ values }); try { - // const authData = await pb.collection(COL_USERS).authWithPassword(values.email, values.password); - await authClient.signInWithPassword({ email: values.email, password: values.password }); + await authClient.signInWithPassword({ + email: values.email, + password: values.password, + // + }); // Refresh the auth state await checkSession?.(); @@ -93,7 +96,7 @@ function AuthLogin(): React.JSX.Element { // UserProvider, for this case, will not refresh the router // After refresh, GuestGuard will handle the redirect - router.push(Paths.AuthorizedTest); + router.push(Paths.StudentMenu); } catch (err: any) { const res_err = err as unknown as ClientResponseError; const { @@ -133,7 +136,12 @@ function AuthLogin(): React.JSX.Element { render={({ field }) => ( <> {'email'} - + field.onChange(e.detail.value)} + onIonBlur={() => field.onBlur()} + /> {errors.email ? ( {errors.email.message} @@ -151,10 +159,14 @@ function AuthLogin(): React.JSX.Element { render={({ field }) => ( <> {'password'} - + field.onChange(e.detail.value)} + onIonBlur={() => field.onBlur()} + /> {errors.password ? ( - {'errors.password.message'} + {errors.password.message} ) : null} diff --git a/002_source/ionic_mobile/src/pages/auth/SignUp/index.tsx b/002_source/ionic_mobile/src/pages/auth/SignUp/index.tsx index 91225cb..b2bde25 100644 --- a/002_source/ionic_mobile/src/pages/auth/SignUp/index.tsx +++ b/002_source/ionic_mobile/src/pages/auth/SignUp/index.tsx @@ -38,6 +38,7 @@ import { z as zod } from 'zod'; import { pb } from '../../../lib/pb'; import { ClientResponseError } from 'pocketbase'; import { COL_USER_METAS, COL_USERS } from '../../../constants'; +import { Paths } from '../../../Paths'; function AuthSignUp(): React.JSX.Element { const params = useParams(); @@ -109,6 +110,8 @@ function AuthSignUp(): React.JSX.Element { }; const userMetaRecord = await pb.collection(COL_USER_METAS).create(userMeta); await pb.collection('users').requestVerification(user.email); + + router.push(Paths.SignUpSuccess); } catch (err: any) { const res_err = err as unknown as ClientResponseError; const { @@ -133,19 +136,7 @@ function AuthSignUp(): React.JSX.Element { return ( - - - - - - - - - - - - - + {/* */}
diff --git a/002_source/ionic_mobile/src/pages/auth/StudentInfo/index.tsx b/002_source/ionic_mobile/src/pages/auth/StudentInfo/index.tsx new file mode 100644 index 0000000..e14dda0 --- /dev/null +++ b/002_source/ionic_mobile/src/pages/auth/StudentInfo/index.tsx @@ -0,0 +1,154 @@ +import { + IonButton, + IonCol, + IonContent, + IonFooter, + IonGrid, + IonHeader, + IonPage, + IonRow, + IonText, + useIonRouter, +} from '@ionic/react'; +import styles from './style.module.scss'; +import { useParams } from 'react-router'; +import { Wave } from '../../../components/Wave'; +import { Paths } from '../../../Paths'; +import React, { useEffect, useState } from 'react'; +import { getUserMetaById } from '../../../db/UserMetas/GetById'; +import { useTranslation } from 'react-i18next'; +import { DBUserMeta } from '../../../db/UserMetas/type'; +import { LoadingScreen } from '../../../components/LoadingScreen'; +import { getStudentAvatarUrl } from '../../../lib/getStudentAvatar'; +import { authClient } from '../../../lib/auth/custom/client'; +import { useUser } from '../../../hooks/use-user'; + +function StudentInfo(): React.JSX.Element { + const router = useIonRouter(); + const { id } = useParams<{ id: string }>(); + const { t } = useTranslation(); + + const [studentMeta, setStudentMeta] = useState(); + const test = useUser(); + + const [showLoading, setShowLoading] = useState(true); + const [showError, setShowError] = useState<{ show: boolean; message: string }>({ show: false, message: '' }); + + function handleBackToLogin() { + router.push(Paths.AuthLogin); + } + + function handleBackOnClick() { + router.push(Paths.Setting); + } + + async function handleFetchUserMeta() { + try { + const result = await getUserMetaById(id); + const tempStudentMeta = result as unknown as DBUserMeta; + + setStudentMeta(tempStudentMeta); + setShowLoading(false); + } catch (error) { + setShowError({ show: true, message: JSON.stringify({ error }, null, 2) }); + setShowLoading(false); + } + } + + async function handleLogoutOnClick() { + try { + await authClient.signOut(); + router.push(Paths.AuthLogin); + } catch (error) { + console.error(error); + } + } + + useEffect(() => { + void handleFetchUserMeta(); + }, []); + + if (showLoading) return ; + if (!studentMeta) return ; + if (showError.show) return <>{showError.message}; + + return ( + + {/* */} + {/* */} + + + + +
+
+
+ {/* */} + + {t('student-name')} + {studentMeta.name} + + {/* */} + + {t('student-email')} + {studentMeta.email} + + {/* */} + + {t('student-phone')} + {studentMeta.phone} + + {/* */} + + {t('back')} + + + + {t('logout')} + +
+
+ {/* */} + + + + + +
+ ); +} + +export default React.memo(StudentInfo); diff --git a/002_source/ionic_mobile/src/pages/auth/StudentInfo/style.module.scss b/002_source/ionic_mobile/src/pages/auth/StudentInfo/style.module.scss new file mode 100644 index 0000000..14ee87f --- /dev/null +++ b/002_source/ionic_mobile/src/pages/auth/StudentInfo/style.module.scss @@ -0,0 +1,17 @@ +.signupPage { + ion-toolbar { + --border-style: none; + --border-color: transparent; + --padding-top: 1rem; + --padding-bottom: 1rem; + --padding-start: 1rem; + --padding-end: 1rem; + } +} + +.headingText { + h5 { + margin-top: 0.2rem; + // color: #d3a6c7; + } +} diff --git a/002_source/ionic_mobile/src/pages/auth/StudentMenu/index.tsx b/002_source/ionic_mobile/src/pages/auth/StudentMenu/index.tsx new file mode 100644 index 0000000..db0384e --- /dev/null +++ b/002_source/ionic_mobile/src/pages/auth/StudentMenu/index.tsx @@ -0,0 +1,71 @@ +import { + IonBackButton, + IonButton, + IonButtons, + IonCardTitle, + IonCol, + IonContent, + IonFooter, + IonGrid, + IonHeader, + IonIcon, + IonInput, + IonLabel, + IonPage, + IonRow, + IonText, + IonToolbar, + useIonRouter, +} from '@ionic/react'; +import styles from './style.module.scss'; +import _ from 'lodash'; +import { Router, useParams } from 'react-router'; +import { Wave } from '../../../components/Wave'; +import { Paths } from '../../../Paths'; +import { useTransition } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useUser } from '../../../hooks/use-user'; + +function StudentMenu(): React.JSX.Element { + const router = useIonRouter(); + const { t } = useTranslation(); + const { user } = useUser(); + + function handleBackToLogin() { + router.push(Paths.AuthLogin); + } + + function handleViewStudentInfoOnClick() { + if (user?.id) { + router.push(Paths.GetStudentInfoLink(user.id)); + } + } + + return ( + + {/* */} + {/* */} + + + Student Menu + {/* */} + + {t('view-student-info')} + + {/* */} + + Back to login + + + + {/* */} + + + + + + + ); +} + +export default StudentMenu; diff --git a/002_source/ionic_mobile/src/pages/auth/StudentMenu/style.module.scss b/002_source/ionic_mobile/src/pages/auth/StudentMenu/style.module.scss new file mode 100644 index 0000000..b66bd24 --- /dev/null +++ b/002_source/ionic_mobile/src/pages/auth/StudentMenu/style.module.scss @@ -0,0 +1,17 @@ +.loginPage { + ion-toolbar { + --border-style: none; + --border-color: transparent; + --padding-top: 1rem; + --padding-bottom: 1rem; + --padding-start: 1rem; + --padding-end: 1rem; + } +} + +.headingText { + h5 { + margin-top: 0.2rem; + // color: #d3a6c7; + } +} diff --git a/002_source/ionic_mobile/src/types/Vocabularies.tsx b/002_source/ionic_mobile/src/types/Vocabularies.tsx index 22376b1..4d448ba 100644 --- a/002_source/ionic_mobile/src/types/Vocabularies.tsx +++ b/002_source/ionic_mobile/src/types/Vocabularies.tsx @@ -1,5 +1,6 @@ // RULES: interface for handling vocabulary record -type Vocabulary = { + +export type Vocabulary = { // id: string; collectionId: string; @@ -14,11 +15,7 @@ type Vocabulary = { image: File[]; sound: File[]; // - expand?: { + expand: { cat_id: LessonCategory; }; }; - -type Vocabularies = Vocabulary[]; - -export default Vocabularies; diff --git a/002_source/ionic_mobile/vite.config.ts b/002_source/ionic_mobile/vite.config.ts index 92e8eea..da932a1 100644 --- a/002_source/ionic_mobile/vite.config.ts +++ b/002_source/ionic_mobile/vite.config.ts @@ -10,6 +10,6 @@ export default defineConfig({ test: { globals: true, environment: 'jsdom', - setupFiles: './src/setupTests.ts' - } + setupFiles: './src/setupTests.ts', + }, }); diff --git a/002_source/pocketbase/docker/dockerfile b/002_source/pocketbase/docker/dockerfile index b6917d3..5ea67c2 100644 --- a/002_source/pocketbase/docker/dockerfile +++ b/002_source/pocketbase/docker/dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3 as downloader +FROM alpine:3 AS downloader ARG TARGETOS ARG TARGETARCH @@ -7,24 +7,28 @@ ARG VERSION ENV BUILDX_ARCH="${TARGETOS:-linux}_${TARGETARCH:-amd64}${TARGETVARIANT}" -RUN wget https://github.com/pocketbase/pocketbase/releases/download/v${VERSION}/pocketbase_${VERSION}_${BUILDX_ARCH}.zip \ - && unzip pocketbase_${VERSION}_${BUILDX_ARCH}.zip \ - && chmod +x /pocketbase +RUN wget https://github.com/pocketbase/pocketbase/releases/download/v${VERSION}/pocketbase_${VERSION}_${BUILDX_ARCH}.zip && \ + unzip pocketbase_${VERSION}_${BUILDX_ARCH}.zip && \ + chmod +x /pocketbase FROM alpine:3 -RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/* +RUN apk update && \ + apk add ca-certificates && \ + rm -rf /var/cache/apk/* EXPOSE 8090 COPY --from=downloader /pocketbase /usr/local/bin/pocketbase +ENTRYPOINT [ + "/usr/local/bin/pocketbase", "serve", "--http=0.0.0.0:8090", "--dir=/pb_data", "--publicDir=/pb_public", "--hooksDir=/pb_hooks"] + + # --- -RUN apk add sqlite entr +# RUN apk add sqlite entr -COPY entrypoint.sh /entrypoint.sh -# ENTRYPOINT ["/entrypoint.sh"] +# # COPY entrypoint.sh /entrypoint.sh +# # ENTRYPOINT ["/entrypoint.sh"] -RUN apk add nodejs yarn -RUN yarn global add nodemon - -ENTRYPOINT ["/usr/local/bin/pocketbase", "serve", "--http=0.0.0.0:8090", "--dir=/pb_data", "--publicDir=/pb_public", "--hooksDir=/pb_hooks"] +# RUN apk add nodejs yarn +# RUN yarn global add nodemon diff --git a/002_source/pocketbase/pb_hooks/seed/050_Customers.js b/002_source/pocketbase/pb_hooks/seed/050_Customers.js index 1e9cbf8..cf06ac4 100644 --- a/002_source/pocketbase/pb_hooks/seed/050_Customers.js +++ b/002_source/pocketbase/pb_hooks/seed/050_Customers.js @@ -143,7 +143,7 @@ module.exports = ($app) => { record.set("timezone", customer[8]); record.set("language", customer[9]); record.set("currency", customer[10]); - record.set("status", customer[11]); + record.set("state", customer[11]); $app.save(record); } diff --git a/002_source/pocketbase/pb_hooks/seed/052_Students.js b/002_source/pocketbase/pb_hooks/seed/052_Students.js index aced88f..7740d8c 100644 --- a/002_source/pocketbase/pb_hooks/seed/052_Students.js +++ b/002_source/pocketbase/pb_hooks/seed/052_Students.js @@ -143,7 +143,7 @@ module.exports = ($app) => { record.set("timezone", student[8]); record.set("language", student[9]); record.set("currency", student[10]); - record.set("status", student[11]); + record.set("state", student[11]); $app.save(record); } diff --git a/002_source/scripts/dc_dev.sh b/002_source/scripts/dc_dev.sh index 82a4f52..29e689a 100755 --- a/002_source/scripts/dc_dev.sh +++ b/002_source/scripts/dc_dev.sh @@ -1,14 +1,14 @@ #!/usr/bin/env bash -DB_COMPOSE_FILE="-f ./docker-compose.db.yml" -_COMPOSE_FILE="-f ./docker-compose.yml" -DEV_COMPOSE_FILE="-f ./docker-compose.dev.yml" +DB_COMPOSE_FILE="-f ./docker/docker-compose.db.yml" +_COMPOSE_FILE="-f ./docker/docker-compose.yml" +DEV_COMPOSE_FILE="-f ./docker/docker-compose.dev.yml" COMPOSE_FILES="$DB_COMPOSE_FILE $_COMPOSE_FILE $DEV_COMPOSE_FILE" set -x -cd docker +# cd docker docker compose $COMPOSE_FILES kill docker compose $COMPOSE_FILES down @@ -21,11 +21,11 @@ docker compose $COMPOSE_FILES logs pocketbase echo "done" -sudo chown 1000:1000 -R ./volumes +sudo chown 1000:1000 -R ./docker/volumes echo "please run yourself !!!!" echo "nodemon -w /pb_hooks --exec "pocketbase seed"" echo "" docker compose $COMPOSE_FILES exec -it pocketbase sh -cd .. +# cd .. diff --git a/003_test/.editorconfig b/003_test/.editorconfig new file mode 100644 index 0000000..0f17867 --- /dev/null +++ b/003_test/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/003_test/.gitignore b/003_test/.gitignore new file mode 100644 index 0000000..da9d21f --- /dev/null +++ b/003_test/.gitignore @@ -0,0 +1,58 @@ +**/*del +**/*bak +**/*log +**/*tmp + +.env +.env.production + +**/*.draft +**/~* +**/*copy*.tsx +**/*copy.tsx + +# **/repomix-output.xml +**/*:Zone.Identifier +**/*.bak +**/*.log +**/*.tmp +**/*.del +**/*.plan +**/_archive + +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem +.swc + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/003_test/.vscode/extensions.json b/003_test/.vscode/extensions.json new file mode 100644 index 0000000..f5b1ea0 --- /dev/null +++ b/003_test/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "foxundermoon.shell-format", + "redhat.vscode-yaml" + ] +} diff --git a/003_test/.vscode/settings.json b/003_test/.vscode/settings.json new file mode 100644 index 0000000..27e8432 --- /dev/null +++ b/003_test/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "[dockerfile]": { + "editor.quickSuggestions": { + "strings": true + }, + "editor.defaultFormatter": "foxundermoon.shell-format" + } +} diff --git a/003_test/001_desktop/01_test_seat/app/.editorconfig b/003_test/001_desktop/01_test_seat/app/.editorconfig new file mode 100644 index 0000000..0f17867 --- /dev/null +++ b/003_test/001_desktop/01_test_seat/app/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/003_test/001_desktop/01_test_seat/app/.gitignore b/003_test/001_desktop/01_test_seat/app/.gitignore new file mode 100644 index 0000000..58786aa --- /dev/null +++ b/003_test/001_desktop/01_test_seat/app/.gitignore @@ -0,0 +1,7 @@ + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/003_test/001_desktop/01_test_seat/app/.prettierrc b/003_test/001_desktop/01_test_seat/app/.prettierrc new file mode 100644 index 0000000..e18b0cf --- /dev/null +++ b/003_test/001_desktop/01_test_seat/app/.prettierrc @@ -0,0 +1,10 @@ +{ + "endOfLine": "lf", + "printWidth": 120, + "quoteProps": "consistent", + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "plugins": [] +} diff --git a/003_test/001_desktop/01_test_seat/app/package-lock.json b/003_test/001_desktop/01_test_seat/app/package-lock.json new file mode 100644 index 0000000..b9d7772 --- /dev/null +++ b/003_test/001_desktop/01_test_seat/app/package-lock.json @@ -0,0 +1,89 @@ +{ + "name": "app", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "app", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "prettier": "^3.5.3" + }, + "devDependencies": { + "@playwright/test": "^1.52.0", + "@types/node": "^22.15.18" + } + }, + "node_modules/@playwright/test": { + "version": "1.52.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.15.18", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/playwright": { + "version": "1.52.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.52.0", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "dev": true, + "license": "MIT" + } + } +} diff --git a/003_test/001_desktop/01_test_seat/app/package.json b/003_test/001_desktop/01_test_seat/app/package.json new file mode 100644 index 0000000..ed00847 --- /dev/null +++ b/003_test/001_desktop/01_test_seat/app/package.json @@ -0,0 +1,17 @@ +{ + "name": "app", + "version": "1.0.0", + "main": "index.js", + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "@playwright/test": "^1.52.0", + "@types/node": "^22.15.18" + }, + "dependencies": { + "prettier": "^3.5.3" + } +} diff --git a/003_test/001_desktop/01_test_seat/app/parking/_GUIDELINE.md b/003_test/001_desktop/01_test_seat/app/parking/_GUIDELINE.md new file mode 100644 index 0000000..e69de29 diff --git a/003_test/001_desktop/01_test_seat/app/parking/example.spec.ts b/003_test/001_desktop/01_test_seat/app/parking/example.spec.ts new file mode 100644 index 0000000..54a906a --- /dev/null +++ b/003_test/001_desktop/01_test_seat/app/parking/example.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@playwright/test'; + +test('has title', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); +}); + +test('get started link', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Click the get started link. + await page.getByRole('link', { name: 'Get started' }).click(); + + // Expects page to have a heading with the name of Installation. + await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +}); diff --git a/003_test/001_desktop/01_test_seat/app/parking/helloworld.spec.ts.demo b/003_test/001_desktop/01_test_seat/app/parking/helloworld.spec.ts.demo new file mode 100644 index 0000000..e77d52a --- /dev/null +++ b/003_test/001_desktop/01_test_seat/app/parking/helloworld.spec.ts.demo @@ -0,0 +1,30 @@ +import { test, expect } from '@playwright/test'; +import { HELLO } from '../_config/helloworld'; + +test('fresh user should appears sign in page', async ({ page }) => { + await page.goto('http://192.168.222.199:3000/dashboard'); + + console.log({ HELLO }); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Sign in | Custom | Auth | demo cms/); +}); + +// test('fresh user login', async ({ page }) => { +// await page.goto('http://192.168.222.199:3000/dashboard'); + +// // Expect a title "to contain" a substring. +// const emailField = page.getByPlaceholder('e.g. admin@123.com'); + +// await emailField.press('Enter'); +// }); + +// test('get started link', async ({ page }) => { +// await page.goto('https://playwright.dev/'); + +// // Click the get started link. +// await page.getByRole('link', { name: 'Get started' }).click(); + +// // Expects page to have a heading with the name of Installation. +// await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +// }); diff --git a/003_test/001_desktop/01_test_seat/app/playwright.config.ts b/003_test/001_desktop/01_test_seat/app/playwright.config.ts new file mode 100644 index 0000000..f7c56f3 --- /dev/null +++ b/003_test/001_desktop/01_test_seat/app/playwright.config.ts @@ -0,0 +1,77 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/003_test/001_desktop/01_test_seat/app/run.sh b/003_test/001_desktop/01_test_seat/app/run.sh new file mode 100755 index 0000000..d9e9233 --- /dev/null +++ b/003_test/001_desktop/01_test_seat/app/run.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -ex + +# npx playwright test +npx playwright --version + +npx playwright test --ui + +npx playwright show-report --host 0.0.0.0 + +echo "done" diff --git a/003_test/001_desktop/01_test_seat/app/scripts/001_setup.sh b/003_test/001_desktop/01_test_seat/app/scripts/001_setup.sh new file mode 100755 index 0000000..3bd6987 --- /dev/null +++ b/003_test/001_desktop/01_test_seat/app/scripts/001_setup.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -ex + +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash + +echo "done" diff --git a/003_test/001_desktop/01_test_seat/app/scripts/002_setup_playwright.sh b/003_test/001_desktop/01_test_seat/app/scripts/002_setup_playwright.sh new file mode 100755 index 0000000..e8cfc3f --- /dev/null +++ b/003_test/001_desktop/01_test_seat/app/scripts/002_setup_playwright.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -ex + +npx playwright install +npx playwright install-deps + +echo "done" diff --git a/003_test/001_desktop/01_test_seat/app/scripts/003_google_chrome.sh b/003_test/001_desktop/01_test_seat/app/scripts/003_google_chrome.sh new file mode 100755 index 0000000..b8dcfe7 --- /dev/null +++ b/003_test/001_desktop/01_test_seat/app/scripts/003_google_chrome.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e + +echo "**** install chrome ****" +apt update +apt install -y wget +wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - +echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list +apt update +apt install -y google-chrome-stable + +echo "install chrome done" diff --git a/003_test/001_desktop/01_test_seat/app/scripts/run_all.sh b/003_test/001_desktop/01_test_seat/app/scripts/run_all.sh new file mode 100755 index 0000000..a3e038e --- /dev/null +++ b/003_test/001_desktop/01_test_seat/app/scripts/run_all.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -ex + +cd scripts +./001_setup.sh 2>&1 | tee setup.log +./002_setup_playwright.sh 2>&1 | tee setup.log +sudo ./003_google_chrome.sh 2>&1 | tee setup.log + +cd .. + +echo "done" diff --git a/003_test/001_desktop/01_test_seat/app/tests-examples/demo-todo-app.spec.ts b/003_test/001_desktop/01_test_seat/app/tests-examples/demo-todo-app.spec.ts new file mode 100644 index 0000000..c25eeb1 --- /dev/null +++ b/003_test/001_desktop/01_test_seat/app/tests-examples/demo-todo-app.spec.ts @@ -0,0 +1,416 @@ +import { test, expect, type Page } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + await page.goto('https://demo.playwright.dev/todomvc'); +}); + +const TODO_ITEMS = ['buy some cheese', 'feed the cat', 'book a doctors appointment'] as const; + +test.describe('New Todo', () => { + test('should allow me to add todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create 1st todo. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Make sure the list only has one todo item. + await expect(page.getByTestId('todo-title')).toHaveText([TODO_ITEMS[0]]); + + // Create 2nd todo. + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + + // Make sure the list now has two todo items. + await expect(page.getByTestId('todo-title')).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); + + test('should clear text input field when an item is added', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create one todo item. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Check that input is empty. + await expect(newTodo).toBeEmpty(); + await checkNumberOfTodosInLocalStorage(page, 1); + }); + + test('should append new items to the bottom of the list', async ({ page }) => { + // Create 3 items. + await createDefaultTodos(page); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count'); + + // Check test using different methods. + await expect(page.getByText('3 items left')).toBeVisible(); + await expect(todoCount).toHaveText('3 items left'); + await expect(todoCount).toContainText('3'); + await expect(todoCount).toHaveText(/3/); + + // Check all items in one call. + await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); + await checkNumberOfTodosInLocalStorage(page, 3); + }); +}); + +test.describe('Mark all as completed', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test.afterEach(async ({ page }) => { + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should allow me to mark all items as completed', async ({ page }) => { + // Complete all todos. + await page.getByLabel('Mark all as complete').check(); + + // Ensure all todos have 'completed' class. + await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + }); + + test('should allow me to clear the complete state of all items', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + // Check and then immediately uncheck. + await toggleAll.check(); + await toggleAll.uncheck(); + + // Should be no completed classes. + await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); + }); + + test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + await toggleAll.check(); + await expect(toggleAll).toBeChecked(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Uncheck first todo. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').uncheck(); + + // Reuse toggleAll locator and make sure its not checked. + await expect(toggleAll).not.toBeChecked(); + + await firstTodo.getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Assert the toggle all is checked again. + await expect(toggleAll).toBeChecked(); + }); +}); + +test.describe('Item', () => { + test('should allow me to mark items as complete', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + // Check first item. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').check(); + await expect(firstTodo).toHaveClass('completed'); + + // Check second item. + const secondTodo = page.getByTestId('todo-item').nth(1); + await expect(secondTodo).not.toHaveClass('completed'); + await secondTodo.getByRole('checkbox').check(); + + // Assert completed class. + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).toHaveClass('completed'); + }); + + test('should allow me to un-mark items as complete', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const firstTodo = page.getByTestId('todo-item').nth(0); + const secondTodo = page.getByTestId('todo-item').nth(1); + const firstTodoCheckbox = firstTodo.getByRole('checkbox'); + + await firstTodoCheckbox.check(); + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await firstTodoCheckbox.uncheck(); + await expect(firstTodo).not.toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 0); + }); + + test('should allow me to edit an item', async ({ page }) => { + await createDefaultTodos(page); + + const todoItems = page.getByTestId('todo-item'); + const secondTodo = todoItems.nth(1); + await secondTodo.dblclick(); + await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); + await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); + + // Explicitly assert the new text value. + await expect(todoItems).toHaveText([TODO_ITEMS[0], 'buy some sausages', TODO_ITEMS[2]]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); +}); + +test.describe('Editing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should hide other controls when editing', async ({ page }) => { + const todoItem = page.getByTestId('todo-item').nth(1); + await todoItem.dblclick(); + await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); + await expect( + todoItem.locator('label', { + hasText: TODO_ITEMS[1], + }) + ).not.toBeVisible(); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should save edits on blur', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); + + await expect(todoItems).toHaveText([TODO_ITEMS[0], 'buy some sausages', TODO_ITEMS[2]]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should trim entered text', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([TODO_ITEMS[0], 'buy some sausages', TODO_ITEMS[2]]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should remove the item if an empty text string was entered', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should cancel edits on escape', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); + await expect(todoItems).toHaveText(TODO_ITEMS); + }); +}); + +test.describe('Counter', () => { + test('should display the current number of todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count'); + + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + await expect(todoCount).toContainText('1'); + + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + await expect(todoCount).toContainText('2'); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); +}); + +test.describe('Clear completed button', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + }); + + test('should display the correct text', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); + }); + + test('should remove completed items when clicked', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).getByRole('checkbox').check(); + await page.getByRole('button', { name: 'Clear completed' }).click(); + await expect(todoItems).toHaveCount(2); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should be hidden when there are no items that are completed', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await page.getByRole('button', { name: 'Clear completed' }).click(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); + }); +}); + +test.describe('Persistence', () => { + test('should persist its data', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const todoItems = page.getByTestId('todo-item'); + const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); + await firstTodoCheck.check(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + + // Ensure there is 1 completed item. + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + // Now reload. + await page.reload(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + }); +}); + +test.describe('Routing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + // make sure the app had a chance to save updated todos in storage + // before navigating to a new view, otherwise the items can get lost :( + // in some frameworks like Durandal + await checkTodosInLocalStorage(page, TODO_ITEMS[0]); + }); + + test('should allow me to display active items', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await expect(todoItem).toHaveCount(2); + await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should respect the back button', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await test.step('Showing all items', async () => { + await page.getByRole('link', { name: 'All' }).click(); + await expect(todoItem).toHaveCount(3); + }); + + await test.step('Showing active items', async () => { + await page.getByRole('link', { name: 'Active' }).click(); + }); + + await test.step('Showing completed items', async () => { + await page.getByRole('link', { name: 'Completed' }).click(); + }); + + await expect(todoItem).toHaveCount(1); + await page.goBack(); + await expect(todoItem).toHaveCount(2); + await page.goBack(); + await expect(todoItem).toHaveCount(3); + }); + + test('should allow me to display completed items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Completed' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(1); + }); + + test('should allow me to display all items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await page.getByRole('link', { name: 'Completed' }).click(); + await page.getByRole('link', { name: 'All' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(3); + }); + + test('should highlight the currently applied filter', async ({ page }) => { + await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); + + //create locators for active and completed links + const activeLink = page.getByRole('link', { name: 'Active' }); + const completedLink = page.getByRole('link', { name: 'Completed' }); + await activeLink.click(); + + // Page change - active items. + await expect(activeLink).toHaveClass('selected'); + await completedLink.click(); + + // Page change - completed items. + await expect(completedLink).toHaveClass('selected'); + }); +}); + +async function createDefaultTodos(page: Page) { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } +} + +async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction((e) => { + return JSON.parse(localStorage['react-todos']).length === e; + }, expected); +} + +async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction((e) => { + return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; + }, expected); +} + +async function checkTodosInLocalStorage(page: Page, title: string) { + return await page.waitForFunction((t) => { + return JSON.parse(localStorage['react-todos']) + .map((todo: any) => todo.title) + .includes(t); + }, title); +} diff --git a/003_test/001_desktop/01_test_seat/app/tests/REQ0016/001_fresh-user-should-appears-sign-in-page.spec.ts b/003_test/001_desktop/01_test_seat/app/tests/REQ0016/001_fresh-user-should-appears-sign-in-page.spec.ts new file mode 100644 index 0000000..7c605d2 --- /dev/null +++ b/003_test/001_desktop/01_test_seat/app/tests/REQ0016/001_fresh-user-should-appears-sign-in-page.spec.ts @@ -0,0 +1,32 @@ +// tests/REQ0006/001_fresh-user-should-appears-sign-in-page.spec.ts +import { test, expect } from '@playwright/test'; +// +import { CMS_HOST, HELLO } from '../_config/helloworld'; +import { TS_HELLO } from '../_test_set/helloworld'; + +test('fresh user should appears sign in page', async ({ page }) => { + console.log({ HELLO, TS_HELLO }); + await page.goto(`${CMS_HOST}/dashboard`); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Sign in | Custom | Auth | demo cms/); +}); + +// test('fresh user login', async ({ page }) => { +// await page.goto('http://192.168.222.199:3000/dashboard'); + +// // Expect a title "to contain" a substring. +// const emailField = page.getByPlaceholder('e.g. admin@123.com'); + +// await emailField.press('Enter'); +// }); + +// test('get started link', async ({ page }) => { +// await page.goto('https://playwright.dev/'); + +// // Click the get started link. +// await page.getByRole('link', { name: 'Get started' }).click(); + +// // Expects page to have a heading with the name of Installation. +// await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +// }); diff --git a/003_test/001_desktop/01_test_seat/app/tests/REQ0016/002_user-login.spec.ts b/003_test/001_desktop/01_test_seat/app/tests/REQ0016/002_user-login.spec.ts new file mode 100644 index 0000000..6e3f699 --- /dev/null +++ b/003_test/001_desktop/01_test_seat/app/tests/REQ0016/002_user-login.spec.ts @@ -0,0 +1,40 @@ +// tests/REQ0006/001_fresh-user-should-appears-sign-in-page.spec.ts +import { test, expect } from '@playwright/test'; +// +import { CMS_HOST, HELLO } from '../_config/helloworld'; +import { TS_HELLO } from '../_test_set/helloworld'; + +test('user login', async ({ page }) => { + console.log({ HELLO, TS_HELLO }); + + await page.goto(`${CMS_HOST}/dashboard`); + await expect(page).toHaveTitle(/Sign in | Custom | Auth | demo cms/); + + await page.getByPlaceholder('name@example.com').pressSequentially('user5@123.com'); + await page.getByPlaceholder('password').pressSequentially('user5@123.com'); + await page.waitForTimeout(1 * 1000); + + await page.getByRole('button', { name: 'Sign in' }).click(); + await page.waitForTimeout(1 * 1000); + + await expect(page).toHaveTitle(/192.168.222.199:3000\/dashboard/); +}); + +// test('fresh user login', async ({ page }) => { +// await page.goto('http://192.168.222.199:3000/dashboard'); + +// // Expect a title "to contain" a substring. +// const emailField = page.getByPlaceholder('e.g. admin@123.com'); + +// await emailField.press('Enter'); +// }); + +// test('get started link', async ({ page }) => { +// await page.goto('https://playwright.dev/'); + +// // Click the get started link. +// await page.getByRole('link', { name: 'Get started' }).click(); + +// // Expects page to have a heading with the name of Installation. +// await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +// }); diff --git a/003_test/001_desktop/01_test_seat/app/tests/_config/helloworld.ts b/003_test/001_desktop/01_test_seat/app/tests/_config/helloworld.ts new file mode 100644 index 0000000..cfa3625 --- /dev/null +++ b/003_test/001_desktop/01_test_seat/app/tests/_config/helloworld.ts @@ -0,0 +1,2 @@ +export const HELLO = 'WORLD'; +export const CMS_HOST = 'http://192.168.222.199:3000'; diff --git a/003_test/001_desktop/01_test_seat/app/tests/_test_set/helloworld.ts b/003_test/001_desktop/01_test_seat/app/tests/_test_set/helloworld.ts new file mode 100644 index 0000000..17efef4 --- /dev/null +++ b/003_test/001_desktop/01_test_seat/app/tests/_test_set/helloworld.ts @@ -0,0 +1 @@ +export const TS_HELLO = 'WORLD'; diff --git a/003_test/001_desktop/01_test_seat/dc_up.sh b/003_test/001_desktop/01_test_seat/dc_up.sh new file mode 100755 index 0000000..0f56a7c --- /dev/null +++ b/003_test/001_desktop/01_test_seat/dc_up.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -ex + +# docker compose build | tee dc_build.log + +# docker compose kill +# docker compose down +docker compose up -d + +echo "done" diff --git a/003_test/001_desktop/01_test_seat/docker-compose.yml b/003_test/001_desktop/01_test_seat/docker-compose.yml new file mode 100644 index 0000000..7347f21 --- /dev/null +++ b/003_test/001_desktop/01_test_seat/docker-compose.yml @@ -0,0 +1,32 @@ +name: 001_desktop + +services: + chromium: + build: . + security_opt: + - seccomp:unconfined #optional + environment: + - TITLE=desktop-test-seat-1 + - FM_HOME=/app + - PUID=1000 # default 911 + - PGID=1000 # default 911 + - TZ=Etc/Asia_HongKong + - CHROME_CLI=https://www.gmail.com/ #optional + # - CUSTOM_PORT=3000 # Internal port the container listens on for http if it needs to be swapped from the default 3000. + - CUSTOM_HTTPS_PORT=3001 # Internal port the container listens on for https if it needs to be swapped from the default 3001. + - NO_FULL=1 # Do not autmatically fullscreen applications when using openbox. + - LC_ALL=en_US.UTF-8 # Set the locale. + - INSTALL_PACKAGES=fonts-noto-cjk + ports: + # - 6000:3000 + - 6001:3001 # https vnc ip + - 9323:9323 # report ip + volumes: + - ./app:/app + shm_size: '1gb' + deploy: + resources: + limits: + cpus: 1 + reservations: + cpus: 0.01 diff --git a/003_test/001_desktop/01_test_seat/dockerfile b/003_test/001_desktop/01_test_seat/dockerfile new file mode 100644 index 0000000..03cd69d --- /dev/null +++ b/003_test/001_desktop/01_test_seat/dockerfile @@ -0,0 +1,27 @@ +FROM ghcr.io/linuxserver/baseimage-kasmvnc:ubuntujammy + +# set version label +ARG BUILD_DATE +ARG VERSION +LABEL build_version="Linuxserver.io version:- ${VERSION} Build-date:- ${BUILD_DATE}" +LABEL maintainer="thelamer" +ARG DEBIAN_FRONTEND="noninteractive" + +# title +ENV TITLE=test-seat + +RUN rm /bin/sh && ln -s /bin/bash /bin/sh + +RUN apt-get update +RUN apt-get install -qqy wget git curl + +# Set up the working directory +WORKDIR /app + +# ports and volumes +EXPOSE 3000 + +VOLUME /config + +COPY ./setup/ /setup +RUN chmod +x /setup/*.sh diff --git a/003_test/001_desktop/01_test_seat/setup/001_nvm.sh b/003_test/001_desktop/01_test_seat/setup/001_nvm.sh new file mode 100644 index 0000000..cd4ef2b --- /dev/null +++ b/003_test/001_desktop/01_test_seat/setup/001_nvm.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -e + +echo "**** install nvm ****" + +# Download and install Node.js (you may need to restart the terminal) +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash + +source ~/.nvm/nvm.sh + +nvm install 20 + +nvm alias default 20 + +nvm use default + +npm install -g yarn + +echo "source ~/.nvm/nvm.sh" >>~/.bashrc + +echo "install nvm done" diff --git a/003_test/001_desktop/01_test_seat/setup/run_all.sh b/003_test/001_desktop/01_test_seat/setup/run_all.sh new file mode 100644 index 0000000..d418834 --- /dev/null +++ b/003_test/001_desktop/01_test_seat/setup/run_all.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -ex + +# ls -1 /setup/0*.sh | while read script; do +# echo "Running $script" +# bash "$script" & +# done + +/setup/001_nvm.sh +sudo /setup/002_google_chrome.sh + +wait + +echo "done" diff --git a/003_test/001_desktop/_GUIDELINES.md b/003_test/001_desktop/_GUIDELINES.md new file mode 100644 index 0000000..c26b0c9 --- /dev/null +++ b/003_test/001_desktop/_GUIDELINES.md @@ -0,0 +1,7 @@ +# GUIDELINES + +this folder contains test and validation of the project + +## highlight + +- `01_test_seat` testing for desktop diff --git a/003_test/002_mobile/01_test_seat/app/.editorconfig b/003_test/002_mobile/01_test_seat/app/.editorconfig new file mode 100644 index 0000000..0f17867 --- /dev/null +++ b/003_test/002_mobile/01_test_seat/app/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/003_test/002_mobile/01_test_seat/app/.gitignore b/003_test/002_mobile/01_test_seat/app/.gitignore new file mode 100644 index 0000000..58786aa --- /dev/null +++ b/003_test/002_mobile/01_test_seat/app/.gitignore @@ -0,0 +1,7 @@ + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/003_test/002_mobile/01_test_seat/app/.prettierrc b/003_test/002_mobile/01_test_seat/app/.prettierrc new file mode 100644 index 0000000..e18b0cf --- /dev/null +++ b/003_test/002_mobile/01_test_seat/app/.prettierrc @@ -0,0 +1,10 @@ +{ + "endOfLine": "lf", + "printWidth": 120, + "quoteProps": "consistent", + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "plugins": [] +} diff --git a/003_test/002_mobile/01_test_seat/app/package-lock.json b/003_test/002_mobile/01_test_seat/app/package-lock.json new file mode 100644 index 0000000..b9d7772 --- /dev/null +++ b/003_test/002_mobile/01_test_seat/app/package-lock.json @@ -0,0 +1,89 @@ +{ + "name": "app", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "app", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "prettier": "^3.5.3" + }, + "devDependencies": { + "@playwright/test": "^1.52.0", + "@types/node": "^22.15.18" + } + }, + "node_modules/@playwright/test": { + "version": "1.52.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.15.18", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/playwright": { + "version": "1.52.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.52.0", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "dev": true, + "license": "MIT" + } + } +} diff --git a/003_test/002_mobile/01_test_seat/app/package.json b/003_test/002_mobile/01_test_seat/app/package.json new file mode 100644 index 0000000..ed00847 --- /dev/null +++ b/003_test/002_mobile/01_test_seat/app/package.json @@ -0,0 +1,17 @@ +{ + "name": "app", + "version": "1.0.0", + "main": "index.js", + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "@playwright/test": "^1.52.0", + "@types/node": "^22.15.18" + }, + "dependencies": { + "prettier": "^3.5.3" + } +} diff --git a/003_test/002_mobile/01_test_seat/app/parking/_GUIDELINE.md b/003_test/002_mobile/01_test_seat/app/parking/_GUIDELINE.md new file mode 100644 index 0000000..e69de29 diff --git a/003_test/002_mobile/01_test_seat/app/parking/example.spec.ts b/003_test/002_mobile/01_test_seat/app/parking/example.spec.ts new file mode 100644 index 0000000..54a906a --- /dev/null +++ b/003_test/002_mobile/01_test_seat/app/parking/example.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@playwright/test'; + +test('has title', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); +}); + +test('get started link', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Click the get started link. + await page.getByRole('link', { name: 'Get started' }).click(); + + // Expects page to have a heading with the name of Installation. + await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +}); diff --git a/003_test/002_mobile/01_test_seat/app/parking/helloworld.spec.ts.demo b/003_test/002_mobile/01_test_seat/app/parking/helloworld.spec.ts.demo new file mode 100644 index 0000000..e77d52a --- /dev/null +++ b/003_test/002_mobile/01_test_seat/app/parking/helloworld.spec.ts.demo @@ -0,0 +1,30 @@ +import { test, expect } from '@playwright/test'; +import { HELLO } from '../_config/helloworld'; + +test('fresh user should appears sign in page', async ({ page }) => { + await page.goto('http://192.168.222.199:3000/dashboard'); + + console.log({ HELLO }); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Sign in | Custom | Auth | demo cms/); +}); + +// test('fresh user login', async ({ page }) => { +// await page.goto('http://192.168.222.199:3000/dashboard'); + +// // Expect a title "to contain" a substring. +// const emailField = page.getByPlaceholder('e.g. admin@123.com'); + +// await emailField.press('Enter'); +// }); + +// test('get started link', async ({ page }) => { +// await page.goto('https://playwright.dev/'); + +// // Click the get started link. +// await page.getByRole('link', { name: 'Get started' }).click(); + +// // Expects page to have a heading with the name of Installation. +// await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +// }); diff --git a/003_test/002_mobile/01_test_seat/app/playwright.config.ts b/003_test/002_mobile/01_test_seat/app/playwright.config.ts new file mode 100644 index 0000000..fe93cae --- /dev/null +++ b/003_test/002_mobile/01_test_seat/app/playwright.config.ts @@ -0,0 +1,77 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + // { + // name: 'chromium', + // use: { ...devices['Desktop Chrome'] }, + // }, + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + /* Test against mobile viewports. */ + { + name: 'Mobile Chrome', + use: { ...devices['Pixel 5'] }, + }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/003_test/002_mobile/01_test_seat/app/run.sh b/003_test/002_mobile/01_test_seat/app/run.sh new file mode 100755 index 0000000..d9e9233 --- /dev/null +++ b/003_test/002_mobile/01_test_seat/app/run.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -ex + +# npx playwright test +npx playwright --version + +npx playwright test --ui + +npx playwright show-report --host 0.0.0.0 + +echo "done" diff --git a/003_test/002_mobile/01_test_seat/app/scripts/001_setup.sh b/003_test/002_mobile/01_test_seat/app/scripts/001_setup.sh new file mode 100755 index 0000000..3bd6987 --- /dev/null +++ b/003_test/002_mobile/01_test_seat/app/scripts/001_setup.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -ex + +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash + +echo "done" diff --git a/003_test/002_mobile/01_test_seat/app/scripts/002_setup_playwright.sh b/003_test/002_mobile/01_test_seat/app/scripts/002_setup_playwright.sh new file mode 100755 index 0000000..e8cfc3f --- /dev/null +++ b/003_test/002_mobile/01_test_seat/app/scripts/002_setup_playwright.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -ex + +npx playwright install +npx playwright install-deps + +echo "done" diff --git a/003_test/002_mobile/01_test_seat/app/scripts/003_google_chrome.sh b/003_test/002_mobile/01_test_seat/app/scripts/003_google_chrome.sh new file mode 100755 index 0000000..b8dcfe7 --- /dev/null +++ b/003_test/002_mobile/01_test_seat/app/scripts/003_google_chrome.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e + +echo "**** install chrome ****" +apt update +apt install -y wget +wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - +echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list +apt update +apt install -y google-chrome-stable + +echo "install chrome done" diff --git a/003_test/002_mobile/01_test_seat/app/scripts/run_all.sh b/003_test/002_mobile/01_test_seat/app/scripts/run_all.sh new file mode 100755 index 0000000..a3e038e --- /dev/null +++ b/003_test/002_mobile/01_test_seat/app/scripts/run_all.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -ex + +cd scripts +./001_setup.sh 2>&1 | tee setup.log +./002_setup_playwright.sh 2>&1 | tee setup.log +sudo ./003_google_chrome.sh 2>&1 | tee setup.log + +cd .. + +echo "done" diff --git a/003_test/002_mobile/01_test_seat/app/tests-examples/demo-todo-app.spec.ts b/003_test/002_mobile/01_test_seat/app/tests-examples/demo-todo-app.spec.ts new file mode 100644 index 0000000..c25eeb1 --- /dev/null +++ b/003_test/002_mobile/01_test_seat/app/tests-examples/demo-todo-app.spec.ts @@ -0,0 +1,416 @@ +import { test, expect, type Page } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + await page.goto('https://demo.playwright.dev/todomvc'); +}); + +const TODO_ITEMS = ['buy some cheese', 'feed the cat', 'book a doctors appointment'] as const; + +test.describe('New Todo', () => { + test('should allow me to add todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create 1st todo. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Make sure the list only has one todo item. + await expect(page.getByTestId('todo-title')).toHaveText([TODO_ITEMS[0]]); + + // Create 2nd todo. + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + + // Make sure the list now has two todo items. + await expect(page.getByTestId('todo-title')).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); + + test('should clear text input field when an item is added', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create one todo item. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Check that input is empty. + await expect(newTodo).toBeEmpty(); + await checkNumberOfTodosInLocalStorage(page, 1); + }); + + test('should append new items to the bottom of the list', async ({ page }) => { + // Create 3 items. + await createDefaultTodos(page); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count'); + + // Check test using different methods. + await expect(page.getByText('3 items left')).toBeVisible(); + await expect(todoCount).toHaveText('3 items left'); + await expect(todoCount).toContainText('3'); + await expect(todoCount).toHaveText(/3/); + + // Check all items in one call. + await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); + await checkNumberOfTodosInLocalStorage(page, 3); + }); +}); + +test.describe('Mark all as completed', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test.afterEach(async ({ page }) => { + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should allow me to mark all items as completed', async ({ page }) => { + // Complete all todos. + await page.getByLabel('Mark all as complete').check(); + + // Ensure all todos have 'completed' class. + await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + }); + + test('should allow me to clear the complete state of all items', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + // Check and then immediately uncheck. + await toggleAll.check(); + await toggleAll.uncheck(); + + // Should be no completed classes. + await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); + }); + + test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + await toggleAll.check(); + await expect(toggleAll).toBeChecked(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Uncheck first todo. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').uncheck(); + + // Reuse toggleAll locator and make sure its not checked. + await expect(toggleAll).not.toBeChecked(); + + await firstTodo.getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Assert the toggle all is checked again. + await expect(toggleAll).toBeChecked(); + }); +}); + +test.describe('Item', () => { + test('should allow me to mark items as complete', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + // Check first item. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').check(); + await expect(firstTodo).toHaveClass('completed'); + + // Check second item. + const secondTodo = page.getByTestId('todo-item').nth(1); + await expect(secondTodo).not.toHaveClass('completed'); + await secondTodo.getByRole('checkbox').check(); + + // Assert completed class. + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).toHaveClass('completed'); + }); + + test('should allow me to un-mark items as complete', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const firstTodo = page.getByTestId('todo-item').nth(0); + const secondTodo = page.getByTestId('todo-item').nth(1); + const firstTodoCheckbox = firstTodo.getByRole('checkbox'); + + await firstTodoCheckbox.check(); + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await firstTodoCheckbox.uncheck(); + await expect(firstTodo).not.toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 0); + }); + + test('should allow me to edit an item', async ({ page }) => { + await createDefaultTodos(page); + + const todoItems = page.getByTestId('todo-item'); + const secondTodo = todoItems.nth(1); + await secondTodo.dblclick(); + await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); + await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); + + // Explicitly assert the new text value. + await expect(todoItems).toHaveText([TODO_ITEMS[0], 'buy some sausages', TODO_ITEMS[2]]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); +}); + +test.describe('Editing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should hide other controls when editing', async ({ page }) => { + const todoItem = page.getByTestId('todo-item').nth(1); + await todoItem.dblclick(); + await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); + await expect( + todoItem.locator('label', { + hasText: TODO_ITEMS[1], + }) + ).not.toBeVisible(); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should save edits on blur', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); + + await expect(todoItems).toHaveText([TODO_ITEMS[0], 'buy some sausages', TODO_ITEMS[2]]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should trim entered text', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([TODO_ITEMS[0], 'buy some sausages', TODO_ITEMS[2]]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should remove the item if an empty text string was entered', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should cancel edits on escape', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); + await expect(todoItems).toHaveText(TODO_ITEMS); + }); +}); + +test.describe('Counter', () => { + test('should display the current number of todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count'); + + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + await expect(todoCount).toContainText('1'); + + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + await expect(todoCount).toContainText('2'); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); +}); + +test.describe('Clear completed button', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + }); + + test('should display the correct text', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); + }); + + test('should remove completed items when clicked', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).getByRole('checkbox').check(); + await page.getByRole('button', { name: 'Clear completed' }).click(); + await expect(todoItems).toHaveCount(2); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should be hidden when there are no items that are completed', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await page.getByRole('button', { name: 'Clear completed' }).click(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); + }); +}); + +test.describe('Persistence', () => { + test('should persist its data', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const todoItems = page.getByTestId('todo-item'); + const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); + await firstTodoCheck.check(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + + // Ensure there is 1 completed item. + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + // Now reload. + await page.reload(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + }); +}); + +test.describe('Routing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + // make sure the app had a chance to save updated todos in storage + // before navigating to a new view, otherwise the items can get lost :( + // in some frameworks like Durandal + await checkTodosInLocalStorage(page, TODO_ITEMS[0]); + }); + + test('should allow me to display active items', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await expect(todoItem).toHaveCount(2); + await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should respect the back button', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await test.step('Showing all items', async () => { + await page.getByRole('link', { name: 'All' }).click(); + await expect(todoItem).toHaveCount(3); + }); + + await test.step('Showing active items', async () => { + await page.getByRole('link', { name: 'Active' }).click(); + }); + + await test.step('Showing completed items', async () => { + await page.getByRole('link', { name: 'Completed' }).click(); + }); + + await expect(todoItem).toHaveCount(1); + await page.goBack(); + await expect(todoItem).toHaveCount(2); + await page.goBack(); + await expect(todoItem).toHaveCount(3); + }); + + test('should allow me to display completed items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Completed' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(1); + }); + + test('should allow me to display all items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await page.getByRole('link', { name: 'Completed' }).click(); + await page.getByRole('link', { name: 'All' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(3); + }); + + test('should highlight the currently applied filter', async ({ page }) => { + await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); + + //create locators for active and completed links + const activeLink = page.getByRole('link', { name: 'Active' }); + const completedLink = page.getByRole('link', { name: 'Completed' }); + await activeLink.click(); + + // Page change - active items. + await expect(activeLink).toHaveClass('selected'); + await completedLink.click(); + + // Page change - completed items. + await expect(completedLink).toHaveClass('selected'); + }); +}); + +async function createDefaultTodos(page: Page) { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } +} + +async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction((e) => { + return JSON.parse(localStorage['react-todos']).length === e; + }, expected); +} + +async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction((e) => { + return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; + }, expected); +} + +async function checkTodosInLocalStorage(page: Page, title: string) { + return await page.waitForFunction((t) => { + return JSON.parse(localStorage['react-todos']) + .map((todo: any) => todo.title) + .includes(t); + }, title); +} diff --git a/003_test/002_mobile/01_test_seat/app/tests/REQ0020/001_fresh-user-should-appears-sign-in-page.spec.ts b/003_test/002_mobile/01_test_seat/app/tests/REQ0020/001_fresh-user-should-appears-sign-in-page.spec.ts new file mode 100644 index 0000000..7c605d2 --- /dev/null +++ b/003_test/002_mobile/01_test_seat/app/tests/REQ0020/001_fresh-user-should-appears-sign-in-page.spec.ts @@ -0,0 +1,32 @@ +// tests/REQ0006/001_fresh-user-should-appears-sign-in-page.spec.ts +import { test, expect } from '@playwright/test'; +// +import { CMS_HOST, HELLO } from '../_config/helloworld'; +import { TS_HELLO } from '../_test_set/helloworld'; + +test('fresh user should appears sign in page', async ({ page }) => { + console.log({ HELLO, TS_HELLO }); + await page.goto(`${CMS_HOST}/dashboard`); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Sign in | Custom | Auth | demo cms/); +}); + +// test('fresh user login', async ({ page }) => { +// await page.goto('http://192.168.222.199:3000/dashboard'); + +// // Expect a title "to contain" a substring. +// const emailField = page.getByPlaceholder('e.g. admin@123.com'); + +// await emailField.press('Enter'); +// }); + +// test('get started link', async ({ page }) => { +// await page.goto('https://playwright.dev/'); + +// // Click the get started link. +// await page.getByRole('link', { name: 'Get started' }).click(); + +// // Expects page to have a heading with the name of Installation. +// await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +// }); diff --git a/003_test/002_mobile/01_test_seat/app/tests/REQ0020/002_user-login.spec.ts b/003_test/002_mobile/01_test_seat/app/tests/REQ0020/002_user-login.spec.ts new file mode 100644 index 0000000..6e3f699 --- /dev/null +++ b/003_test/002_mobile/01_test_seat/app/tests/REQ0020/002_user-login.spec.ts @@ -0,0 +1,40 @@ +// tests/REQ0006/001_fresh-user-should-appears-sign-in-page.spec.ts +import { test, expect } from '@playwright/test'; +// +import { CMS_HOST, HELLO } from '../_config/helloworld'; +import { TS_HELLO } from '../_test_set/helloworld'; + +test('user login', async ({ page }) => { + console.log({ HELLO, TS_HELLO }); + + await page.goto(`${CMS_HOST}/dashboard`); + await expect(page).toHaveTitle(/Sign in | Custom | Auth | demo cms/); + + await page.getByPlaceholder('name@example.com').pressSequentially('user5@123.com'); + await page.getByPlaceholder('password').pressSequentially('user5@123.com'); + await page.waitForTimeout(1 * 1000); + + await page.getByRole('button', { name: 'Sign in' }).click(); + await page.waitForTimeout(1 * 1000); + + await expect(page).toHaveTitle(/192.168.222.199:3000\/dashboard/); +}); + +// test('fresh user login', async ({ page }) => { +// await page.goto('http://192.168.222.199:3000/dashboard'); + +// // Expect a title "to contain" a substring. +// const emailField = page.getByPlaceholder('e.g. admin@123.com'); + +// await emailField.press('Enter'); +// }); + +// test('get started link', async ({ page }) => { +// await page.goto('https://playwright.dev/'); + +// // Click the get started link. +// await page.getByRole('link', { name: 'Get started' }).click(); + +// // Expects page to have a heading with the name of Installation. +// await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +// }); diff --git a/003_test/002_mobile/01_test_seat/app/tests/_config/helloworld.ts b/003_test/002_mobile/01_test_seat/app/tests/_config/helloworld.ts new file mode 100644 index 0000000..cfa3625 --- /dev/null +++ b/003_test/002_mobile/01_test_seat/app/tests/_config/helloworld.ts @@ -0,0 +1,2 @@ +export const HELLO = 'WORLD'; +export const CMS_HOST = 'http://192.168.222.199:3000'; diff --git a/003_test/002_mobile/01_test_seat/app/tests/_test_set/helloworld.ts b/003_test/002_mobile/01_test_seat/app/tests/_test_set/helloworld.ts new file mode 100644 index 0000000..17efef4 --- /dev/null +++ b/003_test/002_mobile/01_test_seat/app/tests/_test_set/helloworld.ts @@ -0,0 +1 @@ +export const TS_HELLO = 'WORLD'; diff --git a/003_test/002_mobile/01_test_seat/dc_up.sh b/003_test/002_mobile/01_test_seat/dc_up.sh new file mode 100755 index 0000000..0f56a7c --- /dev/null +++ b/003_test/002_mobile/01_test_seat/dc_up.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -ex + +# docker compose build | tee dc_build.log + +# docker compose kill +# docker compose down +docker compose up -d + +echo "done" diff --git a/003_test/002_mobile/01_test_seat/docker-compose.yml b/003_test/002_mobile/01_test_seat/docker-compose.yml new file mode 100644 index 0000000..d817fb3 --- /dev/null +++ b/003_test/002_mobile/01_test_seat/docker-compose.yml @@ -0,0 +1,32 @@ +name: 002_mobile + +services: + chromium: + build: . + security_opt: + - seccomp:unconfined #optional + environment: + - TITLE=mobile-test-seat-1 + - FM_HOME=/app + - PUID=1000 # default 911 + - PGID=1000 # default 911 + - TZ=Etc/Asia_HongKong + - CHROME_CLI=https://www.gmail.com/ #optional + # - CUSTOM_PORT=3000 # Internal port the container listens on for http if it needs to be swapped from the default 3000. + - CUSTOM_HTTPS_PORT=3001 # Internal port the container listens on for https if it needs to be swapped from the default 3001. + - NO_FULL=1 # Do n/ot autmatically fullscreen applications when using openbox. + - LC_ALL=en_US.UTF-8 # Set the locale. + - INSTALL_PACKAGES=fonts-noto-cjk + ports: + # - 6000:3000 + - 6002:3001 # https vnc ip + - 9324:9323 # report ip + volumes: + - ./app:/app + shm_size: '1gb' + deploy: + resources: + limits: + cpus: 1 + reservations: + cpus: 0.01 diff --git a/003_test/002_mobile/01_test_seat/dockerfile b/003_test/002_mobile/01_test_seat/dockerfile new file mode 100644 index 0000000..03cd69d --- /dev/null +++ b/003_test/002_mobile/01_test_seat/dockerfile @@ -0,0 +1,27 @@ +FROM ghcr.io/linuxserver/baseimage-kasmvnc:ubuntujammy + +# set version label +ARG BUILD_DATE +ARG VERSION +LABEL build_version="Linuxserver.io version:- ${VERSION} Build-date:- ${BUILD_DATE}" +LABEL maintainer="thelamer" +ARG DEBIAN_FRONTEND="noninteractive" + +# title +ENV TITLE=test-seat + +RUN rm /bin/sh && ln -s /bin/bash /bin/sh + +RUN apt-get update +RUN apt-get install -qqy wget git curl + +# Set up the working directory +WORKDIR /app + +# ports and volumes +EXPOSE 3000 + +VOLUME /config + +COPY ./setup/ /setup +RUN chmod +x /setup/*.sh diff --git a/003_test/002_mobile/01_test_seat/setup/001_nvm.sh b/003_test/002_mobile/01_test_seat/setup/001_nvm.sh new file mode 100644 index 0000000..cd4ef2b --- /dev/null +++ b/003_test/002_mobile/01_test_seat/setup/001_nvm.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -e + +echo "**** install nvm ****" + +# Download and install Node.js (you may need to restart the terminal) +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash + +source ~/.nvm/nvm.sh + +nvm install 20 + +nvm alias default 20 + +nvm use default + +npm install -g yarn + +echo "source ~/.nvm/nvm.sh" >>~/.bashrc + +echo "install nvm done" diff --git a/003_test/002_mobile/01_test_seat/setup/run_all.sh b/003_test/002_mobile/01_test_seat/setup/run_all.sh new file mode 100644 index 0000000..0f90368 --- /dev/null +++ b/003_test/002_mobile/01_test_seat/setup/run_all.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -ex + +# ls -1 /setup/0*.sh | while read script; do +# echo "Running $script" +# bash "$script" & +# done + +/setup/001_nvm.sh & +sudo /setup/002_google_chrome.sh & + +wait + +echo "done" diff --git a/003_test/002_mobile/_GUIDELINES.md b/003_test/002_mobile/_GUIDELINES.md new file mode 100644 index 0000000..949ec20 --- /dev/null +++ b/003_test/002_mobile/_GUIDELINES.md @@ -0,0 +1,7 @@ +# GUIDELINES + +this folder contains test and validation of the project + +## highlight + +- `01_test_seat` testing for mobile diff --git a/003_test/_GUIDELINES.md b/003_test/_GUIDELINES.md new file mode 100644 index 0000000..909b9b7 --- /dev/null +++ b/003_test/_GUIDELINES.md @@ -0,0 +1,9 @@ +# GUIDELINES + +this folder contains test and validation of the project + +## highlight + +- `001_desktop` testing for desktop +- `002_mobile` testing for mobile +- `003_e2e` testing for end-to-end diff --git a/003_test/desktop-test.gif b/003_test/desktop-test.gif new file mode 100644 index 0000000..1fb2813 Binary files /dev/null and b/003_test/desktop-test.gif differ diff --git a/003_test/mobile-test.gif b/003_test/mobile-test.gif new file mode 100644 index 0000000..b1beddc Binary files /dev/null and b/003_test/mobile-test.gif differ diff --git a/003_test/scripts/dc_dev.sh b/003_test/scripts/dc_dev.sh new file mode 100644 index 0000000..c1dae43 --- /dev/null +++ b/003_test/scripts/dc_dev.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -x + +echo "done" diff --git a/003_test/test.code-workspace b/003_test/test.code-workspace new file mode 100644 index 0000000..16f0050 --- /dev/null +++ b/003_test/test.code-workspace @@ -0,0 +1,14 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "../001_documentation" + }, + { + "path": "../000_AI_WORKSPACE" + } + ], + "settings": {} +}