@qlover/create-app 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/bin/create-app.js +28 -0
  3. package/dist/cjs/index.d.ts +67 -0
  4. package/dist/cjs/index.js +1 -0
  5. package/dist/es/index.d.ts +67 -0
  6. package/dist/es/index.js +1 -0
  7. package/package.json +60 -0
  8. package/templates/fe-react/.env +3 -0
  9. package/templates/fe-react/README.md +50 -0
  10. package/templates/fe-react/config/app.common.ts +52 -0
  11. package/templates/fe-react/config/app.router.json +150 -0
  12. package/templates/fe-react/config/feapi.mock.json +14 -0
  13. package/templates/fe-react/config/i18n.ts +21 -0
  14. package/templates/fe-react/config/theme.json +90 -0
  15. package/templates/fe-react/eslint.config.js +31 -0
  16. package/templates/fe-react/index.html +13 -0
  17. package/templates/fe-react/lib/fe-react-controller/FeController.ts +15 -0
  18. package/templates/fe-react/lib/fe-react-controller/index.ts +2 -0
  19. package/templates/fe-react/lib/fe-react-controller/useController.ts +71 -0
  20. package/templates/fe-react/lib/fe-react-theme/ThemeController.ts +40 -0
  21. package/templates/fe-react/lib/fe-react-theme/ThemeStateGetter.ts +53 -0
  22. package/templates/fe-react/lib/fe-react-theme/index.ts +3 -0
  23. package/templates/fe-react/lib/fe-react-theme/tw-generator.js +239 -0
  24. package/templates/fe-react/lib/fe-react-theme/type.ts +21 -0
  25. package/templates/fe-react/lib/openAiApi/OpenAIAuthPlugin.ts +29 -0
  26. package/templates/fe-react/lib/openAiApi/OpenAIClient.ts +51 -0
  27. package/templates/fe-react/lib/openAiApi/StreamProcessor.ts +81 -0
  28. package/templates/fe-react/lib/openAiApi/index.ts +3 -0
  29. package/templates/fe-react/lib/request-common-plugin/index.ts +169 -0
  30. package/templates/fe-react/lib/tw-root10px/index.css +4 -0
  31. package/templates/fe-react/lib/tw-root10px/index.js +178 -0
  32. package/templates/fe-react/package.json +49 -0
  33. package/templates/fe-react/postcss.config.js +6 -0
  34. package/templates/fe-react/public/locales/en/about.json +3 -0
  35. package/templates/fe-react/public/locales/en/common.json +6 -0
  36. package/templates/fe-react/public/locales/en/executor.json +6 -0
  37. package/templates/fe-react/public/locales/en/home.json +10 -0
  38. package/templates/fe-react/public/locales/en/jsonStorage.json +11 -0
  39. package/templates/fe-react/public/locales/en/login.json +7 -0
  40. package/templates/fe-react/public/locales/en/request.json +15 -0
  41. package/templates/fe-react/public/locales/zh/about.json +3 -0
  42. package/templates/fe-react/public/locales/zh/common.json +7 -0
  43. package/templates/fe-react/public/locales/zh/executor.json +7 -0
  44. package/templates/fe-react/public/locales/zh/home.json +10 -0
  45. package/templates/fe-react/public/locales/zh/jsonStorage.json +11 -0
  46. package/templates/fe-react/public/locales/zh/login.json +8 -0
  47. package/templates/fe-react/public/locales/zh/request.json +15 -0
  48. package/templates/fe-react/public/logo.svg +1 -0
  49. package/templates/fe-react/src/App.tsx +20 -0
  50. package/templates/fe-react/src/assets/react.svg +1 -0
  51. package/templates/fe-react/src/components/Loading.tsx +41 -0
  52. package/templates/fe-react/src/components/LocaleLink.tsx +42 -0
  53. package/templates/fe-react/src/components/ProcessProvider.tsx +41 -0
  54. package/templates/fe-react/src/components/ThemeSwitcher.tsx +29 -0
  55. package/templates/fe-react/src/containers/context/BaseRouteContext.ts +27 -0
  56. package/templates/fe-react/src/containers/context/BaseRouteProvider.tsx +11 -0
  57. package/templates/fe-react/src/containers/globals.ts +31 -0
  58. package/templates/fe-react/src/containers/index.ts +71 -0
  59. package/templates/fe-react/src/hooks/useLanguageGuard.ts +25 -0
  60. package/templates/fe-react/src/hooks/useStrictEffect.ts +29 -0
  61. package/templates/fe-react/src/main.tsx +15 -0
  62. package/templates/fe-react/src/pages/404.tsx +14 -0
  63. package/templates/fe-react/src/pages/500.tsx +14 -0
  64. package/templates/fe-react/src/pages/auth/Layout.tsx +14 -0
  65. package/templates/fe-react/src/pages/auth/Login.tsx +62 -0
  66. package/templates/fe-react/src/pages/auth/Register.tsx +3 -0
  67. package/templates/fe-react/src/pages/base/About.tsx +12 -0
  68. package/templates/fe-react/src/pages/base/Executor.tsx +38 -0
  69. package/templates/fe-react/src/pages/base/Home.tsx +78 -0
  70. package/templates/fe-react/src/pages/base/JSONStorage.tsx +124 -0
  71. package/templates/fe-react/src/pages/base/Layout.tsx +17 -0
  72. package/templates/fe-react/src/pages/base/RedirectPathname.tsx +15 -0
  73. package/templates/fe-react/src/pages/base/Request.tsx +91 -0
  74. package/templates/fe-react/src/pages/base/components/BaseHeader.tsx +19 -0
  75. package/templates/fe-react/src/pages/index.tsx +108 -0
  76. package/templates/fe-react/src/services/controllers/ExecutorController.ts +56 -0
  77. package/templates/fe-react/src/services/controllers/JSONStorageController.ts +42 -0
  78. package/templates/fe-react/src/services/controllers/RequestController.ts +105 -0
  79. package/templates/fe-react/src/services/controllers/RouterController.ts +90 -0
  80. package/templates/fe-react/src/services/controllers/UserController.ts +146 -0
  81. package/templates/fe-react/src/services/feApi/FeApi.ts +51 -0
  82. package/templates/fe-react/src/services/feApi/FeApiMockPlugin.ts +42 -0
  83. package/templates/fe-react/src/services/feApi/FeApiType.ts +55 -0
  84. package/templates/fe-react/src/services/feApi/index.ts +2 -0
  85. package/templates/fe-react/src/services/i18n/index.ts +50 -0
  86. package/templates/fe-react/src/services/pageProcesser/PageProcesser.ts +29 -0
  87. package/templates/fe-react/src/services/pageProcesser/index.ts +1 -0
  88. package/templates/fe-react/src/styles/css/index.css +2 -0
  89. package/templates/fe-react/src/styles/css/page.css +3 -0
  90. package/templates/fe-react/src/styles/css/tailwind.css +3 -0
  91. package/templates/fe-react/src/types/Page.ts +49 -0
  92. package/templates/fe-react/src/types/UIDependenciesInterface.ts +31 -0
  93. package/templates/fe-react/src/types/global.d.ts +7 -0
  94. package/templates/fe-react/src/utils/RequestLogger.ts +34 -0
  95. package/templates/fe-react/src/utils/datetime.ts +25 -0
  96. package/templates/fe-react/src/utils/thread.ts +3 -0
  97. package/templates/fe-react/src/vite-env.d.ts +1 -0
  98. package/templates/fe-react/tailwind.config.js +18 -0
  99. package/templates/fe-react/tsconfig.app.json +29 -0
  100. package/templates/fe-react/tsconfig.json +7 -0
  101. package/templates/fe-react/tsconfig.node.json +22 -0
  102. package/templates/fe-react/vite.config.ts +32 -0
  103. package/templates/pack-app/.editorconfig +23 -0
  104. package/templates/pack-app/.env +5 -0
  105. package/templates/pack-app/.env.template +6 -0
  106. package/templates/pack-app/.gitattributes +2 -0
  107. package/templates/pack-app/.github/workflows/general-check.yml +50 -0
  108. package/templates/pack-app/.github/workflows/release.yml.template +110 -0
  109. package/templates/pack-app/.prettierignore +5 -0
  110. package/templates/pack-app/.prettierrc.js +3 -0
  111. package/templates/pack-app/CHANGELOG.md +0 -0
  112. package/templates/pack-app/README.md +1 -0
  113. package/templates/pack-app/eslint.config.js +77 -0
  114. package/templates/pack-app/fe-config.json +10 -0
  115. package/templates/pack-app/jest.config.js +31 -0
  116. package/templates/pack-app/package.json +66 -0
  117. package/templates/pack-app/packages/browser/__tests__/calc.test.ts +9 -0
  118. package/templates/pack-app/packages/browser/package.json +11 -0
  119. package/templates/pack-app/packages/browser/rollup.config.js +70 -0
  120. package/templates/pack-app/packages/browser/src/calc.ts +3 -0
  121. package/templates/pack-app/packages/browser/src/index.ts +1 -0
  122. package/templates/pack-app/packages/browser/tsconfig.json +15 -0
  123. package/templates/pack-app/packages/node/__tests__/readJson.test.ts +25 -0
  124. package/templates/pack-app/packages/node/package.json +11 -0
  125. package/templates/pack-app/packages/node/rollup.config.js +89 -0
  126. package/templates/pack-app/packages/node/src/index.ts +7 -0
  127. package/templates/pack-app/packages/node/src/readJson.ts +6 -0
  128. package/templates/pack-app/packages/node/tsconfig.json +17 -0
  129. package/templates/pack-app/packages/react-vite-lib/README.md +50 -0
  130. package/templates/pack-app/packages/react-vite-lib/__tests__/Sum.test.ts +9 -0
  131. package/templates/pack-app/packages/react-vite-lib/__tests__/Text.test.tsx +11 -0
  132. package/templates/pack-app/packages/react-vite-lib/eslint.config.js +28 -0
  133. package/templates/pack-app/packages/react-vite-lib/index.html +13 -0
  134. package/templates/pack-app/packages/react-vite-lib/package.json +30 -0
  135. package/templates/pack-app/packages/react-vite-lib/public/vite.svg +1 -0
  136. package/templates/pack-app/packages/react-vite-lib/src/calc.ts +3 -0
  137. package/templates/pack-app/packages/react-vite-lib/src/commponents/Text.tsx +7 -0
  138. package/templates/pack-app/packages/react-vite-lib/src/index.ts +2 -0
  139. package/templates/pack-app/packages/react-vite-lib/src/vite-env.d.ts +1 -0
  140. package/templates/pack-app/packages/react-vite-lib/tsconfig.json +25 -0
  141. package/templates/pack-app/packages/react-vite-lib/vite.config.ts +24 -0
  142. package/templates/pack-app/pnpm-workspace.yaml +2 -0
  143. package/templates/pack-app/tsconfig.json +9 -0
  144. package/templates/pack-app/tsconfig.test.json +10 -0
@@ -0,0 +1,17 @@
1
+ import BaseHeader from './components/BaseHeader';
2
+ import { Outlet } from 'react-router-dom';
3
+ import { ProcessProvider } from '@/components/ProcessProvider';
4
+
5
+ export default function Layout() {
6
+ return (
7
+ <ProcessProvider>
8
+ <div data-testid="basic-layout" className="text-base">
9
+ <BaseHeader />
10
+
11
+ <div className="text-black bg-white">
12
+ <Outlet />
13
+ </div>
14
+ </div>
15
+ </ProcessProvider>
16
+ );
17
+ }
@@ -0,0 +1,15 @@
1
+ import { useEffect } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+
4
+ const RedirectToDefault = () => {
5
+ const navigate = useNavigate();
6
+
7
+ useEffect(() => {
8
+ // Redirect to the default language path
9
+ navigate('/en', { replace: true });
10
+ }, [navigate]);
11
+
12
+ return null;
13
+ };
14
+
15
+ export default RedirectToDefault;
@@ -0,0 +1,91 @@
1
+ import { jsonStorageController, requestController } from '@/containers';
2
+ import { useControllerState } from '@lib/fe-react-controller';
3
+ import { useBaseRoutePage } from '../../containers/context/BaseRouteContext';
4
+
5
+ export default function Request() {
6
+ const requestControllerState = useControllerState(requestController);
7
+ const jsonStorageControllerState = useControllerState(jsonStorageController);
8
+ const { t } = useBaseRoutePage();
9
+
10
+ return (
11
+ <div className="min-h-screen bg-gray-100 py-6 flex flex-col sm:py-12">
12
+ <div className="bg-white shadow-lg rounded-lg px-8 py-6">
13
+ {t('requestTimeout')}: {jsonStorageControllerState.requestTimeout}
14
+ </div>
15
+
16
+ <div className="bg-white shadow-lg rounded-lg px-8 py-6">
17
+ <button
18
+ className="bg-blue-500 text-white px-4 py-2 rounded-md"
19
+ onClick={requestController.onHello}
20
+ >
21
+ {requestControllerState.helloState.loading ? 'Loading...' : 'Hello'}
22
+ </button>
23
+ <div className="mt-4">
24
+ {t('helloResult')}:{' '}
25
+ {JSON.stringify(requestControllerState.helloState.result)}
26
+ </div>
27
+ <div className="mt-4">
28
+ {t('helloError')}:{' '}
29
+ {JSON.stringify(requestControllerState.helloState.error)}
30
+ </div>
31
+ </div>
32
+
33
+ <div className="bg-white shadow-lg rounded-lg px-8 py-6">
34
+ <button
35
+ className="bg-blue-500 text-white px-4 py-2 rounded-md"
36
+ onClick={requestController.onIpInfo}
37
+ >
38
+ {requestControllerState.ipInfoState.loading
39
+ ? t('loading')
40
+ : t('ipInfo')}
41
+ </button>
42
+ <div className="mt-4">
43
+ {t('ipInfoResult')}:{' '}
44
+ {JSON.stringify(requestControllerState.ipInfoState.result)}
45
+ </div>
46
+ </div>
47
+
48
+ <div className="bg-white shadow-lg rounded-lg px-8 py-6">
49
+ <button
50
+ className="bg-blue-500 text-white px-4 py-2 rounded-md"
51
+ onClick={requestController.onRandomUser}
52
+ >
53
+ {requestControllerState.randomUserState.loading
54
+ ? t('loading')
55
+ : t('randomUser')}
56
+ </button>
57
+ <div className="mt-4">
58
+ {t('randomUserResult')}:{' '}
59
+ {JSON.stringify(requestControllerState.randomUserState.result)}
60
+ </div>
61
+ <div className="mt-4">
62
+ {t('randomUserError')}:{' '}
63
+ {JSON.stringify(requestControllerState.randomUserState.error)}
64
+ </div>
65
+ </div>
66
+
67
+ <div className="bg-white shadow-lg rounded-lg px-8 py-6">
68
+ <button
69
+ className="bg-blue-500 text-white px-4 py-2 rounded-md"
70
+ onClick={requestController.onTriggerAbortRequest}
71
+ >
72
+ {requestControllerState.abortState.loading
73
+ ? t('stopAbortRequest')
74
+ : t('triggerAbortRequest')}
75
+ <span className="text-xs">
76
+ {requestControllerState.abortState.loading ? t('loading') : ''}
77
+ </span>
78
+ </button>
79
+
80
+ <div className="mt-4">
81
+ {t('abortRequestResult')}:{' '}
82
+ {JSON.stringify(requestControllerState.abortState.result)}
83
+ </div>
84
+ <div className="mt-4">
85
+ {t('abortRequestError')}:{' '}
86
+ {JSON.stringify(requestControllerState.abortState.error)}
87
+ </div>
88
+ </div>
89
+ </div>
90
+ );
91
+ }
@@ -0,0 +1,19 @@
1
+ import ThemeSwitcher from '@/components/ThemeSwitcher';
2
+ import LocaleLink from '@/components/LocaleLink';
3
+
4
+ export default function BaseHeader() {
5
+ return (
6
+ <header className="h-10 bg-white sticky top-0 z-50">
7
+ <div className="flex items-center justify-between h-full px-2">
8
+ <div className="flex items-center">
9
+ <LocaleLink href="/">
10
+ <img src="/logo.svg" alt="logo" />
11
+ </LocaleLink>
12
+ </div>
13
+ <div className="flex items-center">
14
+ <ThemeSwitcher />
15
+ </div>
16
+ </div>
17
+ </header>
18
+ );
19
+ }
@@ -0,0 +1,108 @@
1
+ import { lazy, Suspense } from 'react';
2
+ import isString from 'lodash/isString';
3
+ import { LoadProps, PagesMaps, RouteCategory, RouteType } from '@/types/Page';
4
+ import { Loading } from '@/components/Loading';
5
+ import BaseRouteProvider from '../containers/context/BaseRouteProvider';
6
+ import NotFound from './404';
7
+ import NotFound500 from './500';
8
+
9
+ const staticComponentsMaps: Record<string, () => React.ComponentType<unknown>> =
10
+ {
11
+ '404': () => NotFound as React.ComponentType<unknown>,
12
+ '500': () => NotFound500 as React.ComponentType<unknown>
13
+ };
14
+
15
+ const getRealComponents = () => {
16
+ return import.meta.glob('./*/**/*.tsx');
17
+ };
18
+
19
+ const getLazyComponentMaps = () => {
20
+ const modules = getRealComponents();
21
+
22
+ const pagesMaps: PagesMaps = {};
23
+
24
+ for (const path in modules) {
25
+ const componentName = path.replace(/^\.\/(.*)\.tsx$/, '$1');
26
+
27
+ pagesMaps[componentName] = () =>
28
+ lazy(
29
+ modules[path] as () => Promise<{
30
+ default: React.ComponentType<unknown>;
31
+ }>
32
+ );
33
+ }
34
+
35
+ return pagesMaps;
36
+ };
37
+
38
+ // 懒加载组件
39
+ const lazyLoad = ({ pagesMaps, componentPath, route, Provider }: LoadProps) => {
40
+ // first try static
41
+ let loadedComponent = staticComponentsMaps[componentPath];
42
+ if (!loadedComponent) {
43
+ loadedComponent = pagesMaps[componentPath];
44
+ }
45
+
46
+ if (!loadedComponent) {
47
+ console.warn(`Route ${componentPath} not found`);
48
+ return <NotFound route={componentPath} />;
49
+ }
50
+
51
+ const Component = loadedComponent();
52
+
53
+ return (
54
+ <Suspense fallback={<Loading fullscreen />}>
55
+ {Provider ? (
56
+ <Provider {...route.meta}>
57
+ <Component />
58
+ </Provider>
59
+ ) : (
60
+ <Component />
61
+ )}
62
+ </Suspense>
63
+ );
64
+ };
65
+
66
+ // 转换单个路由
67
+ const transformRoute = (
68
+ route: RouteType,
69
+ options: Pick<LoadProps, 'pagesMaps' | 'Provider'>
70
+ ): RouteType => {
71
+ const result: RouteType = {
72
+ ...route,
73
+ path: route.path
74
+ };
75
+
76
+ if (isString(route.element)) {
77
+ result.element = lazyLoad({
78
+ ...options,
79
+ componentPath: route.element,
80
+ route
81
+ });
82
+ }
83
+
84
+ if (route.children) {
85
+ result.children = route.children.map((child) =>
86
+ transformRoute(child, options)
87
+ );
88
+ }
89
+
90
+ return result;
91
+ };
92
+
93
+ export function createFeReactRoutes(routes: RouteType[]) {
94
+ const pagesMaps = getLazyComponentMaps();
95
+
96
+ return routes.map((route) =>
97
+ transformRoute(route, { pagesMaps, Provider: BaseRouteProvider })
98
+ );
99
+ }
100
+
101
+ export function filterRoutesByCategory(
102
+ routes: RouteType[],
103
+ category: RouteCategory[]
104
+ ): RouteType[] {
105
+ return routes.filter((route) =>
106
+ route.meta?.category ? category.includes(route.meta?.category) : true
107
+ );
108
+ }
@@ -0,0 +1,56 @@
1
+ import { FeController } from '@lib/fe-react-controller';
2
+ import { FeApi } from '@/services/feApi';
3
+ import cloneDeep from 'lodash/cloneDeep';
4
+ import {
5
+ ExecutorPlugin,
6
+ RequestAdapterResponse
7
+ } from 'packages/fe-utils/interface';
8
+ import { RequestAdapterFetchConfig } from '@qlover/fe-utils';
9
+
10
+ function createDefaultState() {
11
+ return {
12
+ helloState: {
13
+ loading: false,
14
+ result: null as unknown,
15
+ error: null as unknown
16
+ }
17
+ };
18
+ }
19
+
20
+ export type ExecutorControllerState = ReturnType<typeof createDefaultState>;
21
+
22
+ const TestPlugin: ExecutorPlugin<RequestAdapterFetchConfig> = {
23
+ pluginName: 'test',
24
+ async onSuccess({ parameters, returnValue }) {
25
+ if (
26
+ parameters.responseType === 'text' &&
27
+ (returnValue as RequestAdapterResponse).data instanceof Response
28
+ ) {
29
+ (returnValue as RequestAdapterResponse<unknown, string>).data = await (
30
+ returnValue as RequestAdapterResponse<unknown, Response>
31
+ ).data.text();
32
+ }
33
+ }
34
+ };
35
+
36
+ export class ExecutorController extends FeController<ExecutorControllerState> {
37
+ private feApi: FeApi;
38
+
39
+ constructor(feApi: FeApi) {
40
+ super(createDefaultState());
41
+
42
+ this.feApi = cloneDeep(feApi);
43
+
44
+ this.feApi.usePlugin(TestPlugin);
45
+ }
46
+
47
+ onTestPlugins = async () => {
48
+ console.log('onTestPlugins');
49
+
50
+ const res = await this.feApi.get('/api/v1/executor/test', {
51
+ responseType: 'text'
52
+ });
53
+
54
+ console.log('res', res);
55
+ };
56
+ }
@@ -0,0 +1,42 @@
1
+ import { FeController } from '@lib/fe-react-controller';
2
+ import { JSONStorage } from '@qlover/fe-utils';
3
+ import { random } from 'lodash';
4
+
5
+ interface JSONStoragePageState {
6
+ testKey1?: number;
7
+ testKey2?: number;
8
+ expireTime: number;
9
+ requestTimeout: number;
10
+ }
11
+
12
+ export class JSONStorageController extends FeController<JSONStoragePageState> {
13
+ constructor(private storage: JSONStorage) {
14
+ super({
15
+ testKey1: storage.getItem('testKey1') ?? 0,
16
+ testKey2: storage.getItem('testKey2') ?? 0,
17
+ expireTime: 5000,
18
+ requestTimeout: storage.getItem('requestTimeout') ?? 5000
19
+ });
20
+ }
21
+
22
+ changeRandomTestKey1 = (): void => {
23
+ const value = random(100, 9000);
24
+ this.storage.setItem('testKey1', value);
25
+ this.emit({ ...this.state, testKey1: value });
26
+ };
27
+
28
+ onChangeRandomTestKey2 = (): void => {
29
+ const value = random(100, 9000);
30
+ this.storage.setItem('testKey2', value, Date.now() + this.state.expireTime);
31
+ this.emit({ ...this.state, testKey2: value });
32
+ };
33
+
34
+ changeExpireTime = (expireTime: number): void => {
35
+ this.emit({ ...this.state, expireTime });
36
+ };
37
+
38
+ changeRequestTimeout = (requestTimeout: number): void => {
39
+ this.storage.setItem('requestTimeout', requestTimeout);
40
+ this.emit({ ...this.state, requestTimeout });
41
+ };
42
+ }
@@ -0,0 +1,105 @@
1
+ import { OpenAIClient } from '@lib/openAiApi/OpenAIClient';
2
+ import { FeController } from '@lib/fe-react-controller';
3
+ import { FeApi } from '@/services/feApi';
4
+ import { logger } from '@/containers/globals';
5
+
6
+ function createDefaultState() {
7
+ return {
8
+ helloState: {
9
+ loading: false,
10
+ result: null as unknown,
11
+ error: null as unknown
12
+ },
13
+ ipInfoState: {
14
+ loading: false,
15
+ result: null as unknown,
16
+ error: null as unknown
17
+ },
18
+ randomUserState: {
19
+ loading: false,
20
+ result: null as unknown,
21
+ error: null as unknown
22
+ },
23
+ abortState: {
24
+ loading: false,
25
+ result: null as unknown,
26
+ error: null as unknown
27
+ }
28
+ };
29
+ }
30
+
31
+ export type RequestControllerState = ReturnType<typeof createDefaultState>;
32
+
33
+ export class RequestController extends FeController<RequestControllerState> {
34
+ constructor(
35
+ private readonly aiApi: OpenAIClient,
36
+ private readonly feApi: FeApi
37
+ ) {
38
+ super(createDefaultState());
39
+ }
40
+
41
+ onHello = async () => {
42
+ this.setState({ helloState: { loading: true, result: '', error: null } });
43
+
44
+ try {
45
+ const result = await this.aiApi.completion({
46
+ messages: [{ role: 'user', content: 'Hello, world!' }]
47
+ });
48
+ this.setState({ helloState: { loading: false, result, error: null } });
49
+ } catch (error) {
50
+ logger.error(error);
51
+ this.setState({ helloState: { loading: false, result: null, error } });
52
+ }
53
+ };
54
+
55
+ onIpInfo = async () => {
56
+ this.setState({
57
+ ipInfoState: { loading: true, result: null, error: null }
58
+ });
59
+ try {
60
+ const result = await this.feApi.getIpInfo();
61
+ this.setState({ ipInfoState: { loading: false, result, error: null } });
62
+ } catch (error) {
63
+ logger.error(error);
64
+ this.setState({ ipInfoState: { loading: false, result: null, error } });
65
+ }
66
+ };
67
+
68
+ onRandomUser = async () => {
69
+ this.setState({
70
+ randomUserState: { loading: true, result: null, error: null }
71
+ });
72
+ try {
73
+ const result = await this.feApi.getRandomUser();
74
+ this.setState({
75
+ randomUserState: { loading: false, result, error: null }
76
+ });
77
+ } catch (error) {
78
+ logger.error(error);
79
+ this.setState({
80
+ randomUserState: { loading: false, result: null, error }
81
+ });
82
+ }
83
+ };
84
+
85
+ onTriggerAbortRequest = async () => {
86
+ if (this.state.abortState.loading) {
87
+ this.stopAbortRequest();
88
+ return;
89
+ }
90
+
91
+ this.setState({ abortState: { loading: true, result: null, error: null } });
92
+ try {
93
+ await this.feApi.request({
94
+ method: 'GET',
95
+ url: 'https://api.example.com/users'
96
+ });
97
+ } catch (error) {
98
+ this.setState({ abortState: { loading: false, result: null, error } });
99
+ }
100
+ };
101
+
102
+ stopAbortRequest = async () => {
103
+ this.feApi.stop({ method: 'GET', url: 'https://api.example.com/users' });
104
+ };
105
+ }
@@ -0,0 +1,90 @@
1
+ import { I18nService } from '@/services/i18n';
2
+ import { RouteConfig, RouteType } from '@/types/Page';
3
+ import { UIDependenciesInterface } from '@/types/UIDependenciesInterface';
4
+ import { i18nConfig } from '@config/i18n';
5
+ import { Logger } from '@qlover/fe-utils';
6
+ import { NavigateFunction, NavigateOptions } from 'react-router-dom';
7
+
8
+ export type RouterControllerDependencies = {
9
+ location: globalThis.Location;
10
+ navigate: NavigateFunction;
11
+ };
12
+
13
+ export type RouterControllerOptions = {
14
+ config: RouteConfig;
15
+ logger: Logger;
16
+ };
17
+
18
+ export type RouterControllerState = {
19
+ routes: RouteType[];
20
+ };
21
+
22
+ export class RouterController
23
+ implements UIDependenciesInterface<RouterControllerDependencies>
24
+ {
25
+ /**
26
+ * @override
27
+ */
28
+ dependencies?: RouterControllerDependencies;
29
+
30
+ state: RouterControllerState;
31
+
32
+ constructor(private options: RouterControllerOptions) {
33
+ this.state = {
34
+ routes: options.config.routes
35
+ };
36
+ }
37
+
38
+ get logger(): Logger {
39
+ return this.options.logger;
40
+ }
41
+
42
+ get navigate(): NavigateFunction | undefined {
43
+ const navigate = this.dependencies?.navigate;
44
+
45
+ if (!navigate) {
46
+ this.logger.debug('navigate is not set');
47
+ }
48
+
49
+ return navigate;
50
+ }
51
+
52
+ /**
53
+ * @override
54
+ */
55
+ setDependencies(dependencies: Partial<RouterControllerDependencies>): void {
56
+ this.dependencies = Object.assign(
57
+ this.dependencies || {},
58
+ dependencies
59
+ ) as RouterControllerDependencies;
60
+ }
61
+
62
+ getRoutes(): RouteType[] {
63
+ return this.state.routes;
64
+ }
65
+
66
+ changeRoutes(routes: RouteType[]): void {
67
+ this.state.routes = routes;
68
+ }
69
+
70
+ goto(path: string, options?: NavigateOptions): void {
71
+ console.trace('path');
72
+
73
+ const tryLng = path.split('/')[0];
74
+ if (!I18nService.isValidLanguage(tryLng)) {
75
+ path = i18nConfig.fallbackLng + path.replace(tryLng, '');
76
+ }
77
+
78
+ this.navigate?.(path, options);
79
+
80
+ this.navigate?.(path, options);
81
+ }
82
+
83
+ gotoLogin(): void {
84
+ this.goto('/login', { replace: true });
85
+ }
86
+
87
+ replaceToHome(): void {
88
+ this.goto('/', { replace: true });
89
+ }
90
+ }
@@ -0,0 +1,146 @@
1
+ import { ExecutorPlugin, JSONStorage } from '@qlover/fe-utils';
2
+ import { sleep } from '@/utils/thread';
3
+ import { FeController } from '@lib/fe-react-controller';
4
+ import { FeApi, } from '@/services/feApi';
5
+ import { FeApiGetUserInfo, FeApiLogin } from '@/services/feApi/FeApiType';
6
+ import { adjustExpirationTime } from '@/utils/datetime';
7
+ import { RouterController } from './RouterController';
8
+
9
+ export interface UserControllerState {
10
+ success: boolean;
11
+ userInfo: FeApiGetUserInfo['response']['data'];
12
+ }
13
+
14
+ export interface UserControllerOptions {
15
+ /**
16
+ * @default `month`
17
+ */
18
+ expiresIn?: number | 'day' | 'week' | 'month' | 'year';
19
+ storageKey: string;
20
+ storage: JSONStorage;
21
+ feApi: FeApi;
22
+ routerController: RouterController;
23
+ }
24
+
25
+ interface LoginInterface {
26
+ login(
27
+ params: FeApiLogin['request']
28
+ ): Promise<FeApiGetUserInfo['response']['data']>;
29
+ logout(): void;
30
+ }
31
+
32
+ function restoreUserSession(options: UserControllerOptions): {
33
+ state: UserControllerState;
34
+ token: string;
35
+ } {
36
+ const { storageKey, storage } = options;
37
+ const token = storage.getItem(storageKey, '') as string;
38
+
39
+ return {
40
+ state: {
41
+ success: !!token,
42
+ userInfo: {
43
+ name: '',
44
+ email: '',
45
+ picture: ''
46
+ }
47
+ },
48
+ token
49
+ };
50
+ }
51
+
52
+ export class UserController
53
+ extends FeController<UserControllerState>
54
+ implements ExecutorPlugin, LoginInterface
55
+ {
56
+ readonly pluginName = 'UserController';
57
+
58
+ private token = '';
59
+
60
+ constructor(private options: UserControllerOptions) {
61
+ const restoreSession = restoreUserSession(options);
62
+ super(restoreSession.state);
63
+
64
+ this.token = restoreSession.token;
65
+ }
66
+
67
+ selectorSuccess = (state: UserControllerState): boolean => state.success;
68
+
69
+ /**
70
+ * @override
71
+ */
72
+ async onBefore(): Promise<void> {
73
+ await sleep(1000);
74
+
75
+ if (!this.token) {
76
+ throw new Error('User not logged in');
77
+ }
78
+
79
+ const userInfo = await this.options.feApi.getUserInfo();
80
+
81
+ this.setState({
82
+ success: true,
83
+ userInfo: userInfo.data
84
+ });
85
+ }
86
+
87
+ /**
88
+ * @override
89
+ */
90
+ async onError(): Promise<void> {
91
+ this.logout();
92
+
93
+ this.options.routerController.gotoLogin();
94
+ }
95
+
96
+ /**
97
+ * @override
98
+ */
99
+ async login(
100
+ params: FeApiLogin['request']
101
+ ): Promise<FeApiGetUserInfo['response']['data']> {
102
+ const { feApi } = this.options;
103
+
104
+ const result = await feApi.login(params);
105
+
106
+ this.setToken(result.data.token);
107
+
108
+ const userInfo = await feApi.getUserInfo();
109
+
110
+ this.setState({
111
+ success: true,
112
+ userInfo: userInfo.data
113
+ });
114
+
115
+ return userInfo.data;
116
+ }
117
+
118
+ /**
119
+ * @override
120
+ */
121
+ logout(): void {
122
+ this.reset();
123
+ }
124
+
125
+ setToken(token: string): void {
126
+ this.token = token;
127
+
128
+ const { storageKey, storage } = this.options;
129
+
130
+ storage.setItem(
131
+ storageKey,
132
+ this.token,
133
+ adjustExpirationTime(Date.now(), this.options.expiresIn ?? 'month')
134
+ );
135
+ }
136
+
137
+ reset(): void {
138
+ this.token = '';
139
+ this.options.storage.removeItem(this.options.storageKey);
140
+ this.setState({ success: false });
141
+ }
142
+
143
+ isAuthenticated(): boolean {
144
+ return this.state.success;
145
+ }
146
+ }