@qlover/create-app 0.4.0 → 0.4.2

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 (46) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/configs/_common/package.json.template +1 -1
  3. package/dist/index.js +1 -1
  4. package/package.json +1 -1
  5. package/templates/react-app/.env +1 -2
  6. package/templates/react-app/README.md +70 -8
  7. package/templates/react-app/config/app.router.json +13 -13
  8. package/templates/react-app/config/common.ts +8 -0
  9. package/templates/react-app/config/i18n.ts +3 -1
  10. package/templates/react-app/config/{Identifier.I18n.ts → identifier/I18n.ts} +321 -3
  11. package/templates/react-app/index.html +1 -1
  12. package/templates/react-app/public/locales/en/common.json +54 -8
  13. package/templates/react-app/public/locales/zh/common.json +54 -8
  14. package/templates/react-app/public/router-root/logo.svg +1 -0
  15. package/templates/react-app/src/App.tsx +6 -3
  16. package/templates/react-app/src/base/cases/PublicAssetsPath.ts +17 -0
  17. package/templates/react-app/src/base/port/LoginInterface.ts +8 -0
  18. package/templates/react-app/src/base/port/StoreInterface.ts +58 -0
  19. package/templates/react-app/src/base/services/I18nService.ts +15 -9
  20. package/templates/react-app/src/{uikit/controllers/RouterController.ts → base/services/RouteService.ts} +12 -12
  21. package/templates/react-app/src/{uikit/controllers/UserController.ts → base/services/UserService.ts} +21 -11
  22. package/templates/react-app/src/core/bootstrap.ts +1 -1
  23. package/templates/react-app/src/core/registers/RegisterCommon.ts +11 -1
  24. package/templates/react-app/src/core/registers/RegisterControllers.ts +3 -12
  25. package/templates/react-app/src/pages/auth/Layout.tsx +5 -5
  26. package/templates/react-app/src/pages/auth/Login.tsx +50 -29
  27. package/templates/react-app/src/pages/auth/Register.tsx +238 -1
  28. package/templates/react-app/src/pages/base/About.tsx +1 -1
  29. package/templates/react-app/src/pages/base/ErrorIdentifier.tsx +2 -2
  30. package/templates/react-app/src/pages/base/Executor.tsx +4 -4
  31. package/templates/react-app/src/pages/base/Home.tsx +1 -1
  32. package/templates/react-app/src/pages/base/JSONStorage.tsx +3 -3
  33. package/templates/react-app/src/pages/base/Request.tsx +4 -4
  34. package/templates/react-app/src/pages/base/components/BaseHeader.tsx +8 -2
  35. package/templates/react-app/src/uikit/components/LanguageSwitcher.tsx +7 -7
  36. package/templates/react-app/src/uikit/components/ThemeSwitcher.tsx +3 -3
  37. package/templates/react-app/src/uikit/contexts/BaseRouteContext.ts +1 -1
  38. package/templates/react-app/src/uikit/controllers/ExecutorController.ts +6 -3
  39. package/templates/react-app/src/uikit/controllers/JSONStorageController.ts +6 -3
  40. package/templates/react-app/src/uikit/controllers/RequestController.ts +3 -4
  41. package/templates/react-app/src/uikit/hooks/useDocumentTitle.ts +15 -0
  42. package/templates/react-app/src/uikit/hooks/useStore.ts +12 -0
  43. package/templates/react-app/src/uikit/providers/BaseRouteProvider.tsx +7 -1
  44. package/templates/react-app/src/uikit/providers/ProcessProvider.tsx +7 -7
  45. package/templates/react-app/vite.config.ts +20 -11
  46. /package/templates/react-app/config/{Identifier.Error.ts → identifier/Error.ts} +0 -0
@@ -0,0 +1,58 @@
1
+ import { SliceStore } from '@qlover/slice-store-react';
2
+
3
+ /**
4
+ * Store State Interface
5
+ *
6
+ * This interface is used to define the state of a store.
7
+ * It is used to define the state of a store.
8
+ *
9
+ * must implement the copyWith method
10
+ */
11
+ export interface StoreStateInterface {
12
+ // You can define your own properties here
13
+ // ...
14
+ }
15
+
16
+ /**
17
+ * Store Interface
18
+ *
19
+ * This class is used to define the store interface.
20
+ * It is used to define the store interface.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * class ChatStoreState implements StoreStateInterface {
25
+ * isChatRunning: boolean = false;
26
+ *
27
+ * copyWith(state: { isChatRunning?: boolean }): this {
28
+ * return Object.assign(new ChatStoreState(), this, state);
29
+ * }
30
+ * }
31
+ *
32
+ * export class ChatStore extends StoreInterface<ChatStoreState> {
33
+ * constructor() {
34
+ * super(() => new ChatStoreState());
35
+ * }
36
+ * }
37
+ * ```
38
+ */
39
+ export abstract class StoreInterface<
40
+ T extends StoreStateInterface
41
+ > extends SliceStore<T> {
42
+ /**
43
+ * Constructor
44
+ *
45
+ * @param stateFactory - The factory function to create the initial state
46
+ */
47
+ constructor(protected stateFactory: () => T) {
48
+ super(stateFactory);
49
+ }
50
+
51
+ /**
52
+ * Reset the state of the store
53
+ */
54
+ resetState(): void {
55
+ // Create a new instance of initial state
56
+ this.emit(this.stateFactory());
57
+ }
58
+ }
@@ -4,22 +4,22 @@ import LanguageDetector from 'i18next-browser-languagedetector';
4
4
  import HttpApi from 'i18next-http-backend';
5
5
  import merge from 'lodash/merge';
6
6
  import i18nConfig from '@config/i18n';
7
+ import type { BootstrapExecutorPlugin } from '@qlover/corekit-bridge';
7
8
  import {
8
- SliceStore,
9
- type BootstrapExecutorPlugin
10
- } from '@qlover/corekit-bridge';
11
-
9
+ StoreInterface,
10
+ StoreStateInterface
11
+ } from '@/base/port/StoreInterface';
12
12
  const { supportedLngs, fallbackLng } = i18nConfig;
13
13
 
14
14
  export type I18nServiceLocale = (typeof supportedLngs)[number];
15
15
 
16
- export class I18nServiceState {
16
+ export class I18nServiceState implements StoreStateInterface {
17
17
  loading: boolean = false;
18
18
  constructor(public language: I18nServiceLocale) {}
19
19
  }
20
20
 
21
21
  export class I18nService
22
- extends SliceStore<I18nServiceState>
22
+ extends StoreInterface<I18nServiceState>
23
23
  implements BootstrapExecutorPlugin
24
24
  {
25
25
  readonly pluginName = 'I18nService';
@@ -57,9 +57,15 @@ export class I18nService
57
57
  const pathLanguageDetector = {
58
58
  name: 'pathLanguageDetector',
59
59
  lookup: () => {
60
- const path = this.pathname.split('/');
61
- const language = path[1];
62
- return I18nService.isValidLanguage(language) ? language : fallbackLng;
60
+ const paths = this.pathname.split('/');
61
+
62
+ for (const path of paths) {
63
+ if (I18nService.isValidLanguage(path)) {
64
+ return path;
65
+ }
66
+ }
67
+
68
+ return fallbackLng;
63
69
  },
64
70
  cacheUserLanguage() {
65
71
  // no cache, because we get language from URL
@@ -4,32 +4,32 @@ import type { UIDependenciesInterface } from '@/base/port/UIDependenciesInterfac
4
4
  import type { LoggerInterface } from '@qlover/logger';
5
5
  import { I18nService } from '@/base/services/I18nService';
6
6
 
7
- export type RouterControllerDependencies = {
7
+ export type RouterServiceDependencies = {
8
8
  navigate: NavigateFunction;
9
9
  };
10
10
 
11
- export type RouterControllerOptions = {
11
+ export type RouterServiceOptions = {
12
12
  config: {
13
13
  routes: RouteConfigValue[];
14
14
  };
15
15
  logger: LoggerInterface;
16
16
  };
17
17
 
18
- export type RouterControllerState = {
18
+ export type RouterServiceState = {
19
19
  routes: RouteConfigValue[];
20
20
  };
21
21
 
22
- export class RouterController
23
- implements UIDependenciesInterface<RouterControllerDependencies>
22
+ export class RouteService
23
+ implements UIDependenciesInterface<RouterServiceDependencies>
24
24
  {
25
25
  /**
26
26
  * @override
27
27
  */
28
- dependencies?: RouterControllerDependencies;
28
+ dependencies?: RouterServiceDependencies;
29
29
 
30
- state: RouterControllerState;
30
+ state: RouterServiceState;
31
31
 
32
- constructor(private options: RouterControllerOptions) {
32
+ constructor(private options: RouterServiceOptions) {
33
33
  this.state = {
34
34
  routes: options.config.routes
35
35
  };
@@ -49,7 +49,7 @@ export class RouterController
49
49
  return navigate;
50
50
  }
51
51
 
52
- static composePath(path: string): string {
52
+ composePath(path: string): string {
53
53
  const targetLang = I18nService.getCurrentLanguage();
54
54
  return `/${targetLang}${path}`;
55
55
  }
@@ -57,11 +57,11 @@ export class RouterController
57
57
  /**
58
58
  * @override
59
59
  */
60
- setDependencies(dependencies: Partial<RouterControllerDependencies>): void {
60
+ setDependencies(dependencies: Partial<RouterServiceDependencies>): void {
61
61
  this.dependencies = Object.assign(
62
62
  this.dependencies || {},
63
63
  dependencies
64
- ) as RouterControllerDependencies;
64
+ ) as RouterServiceDependencies;
65
65
  }
66
66
 
67
67
  getRoutes(): RouteConfigValue[] {
@@ -73,7 +73,7 @@ export class RouterController
73
73
  }
74
74
 
75
75
  goto(path: string, options?: NavigateOptions): void {
76
- path = RouterController.composePath(path);
76
+ path = this.composePath(path);
77
77
  this.logger.debug('Goto path => ', path);
78
78
  this.navigate?.(path, options);
79
79
  }
@@ -3,17 +3,17 @@ import type {
3
3
  UserApiGetUserInfoTransaction,
4
4
  UserApiLoginTransaction
5
5
  } from '@/base/apis/userApi/UserApiType';
6
- import { RouterController } from './RouterController';
6
+ import { RouteService } from './RouteService';
7
7
  import { ThreadUtil, type StorageTokenInterface } from '@qlover/corekit-bridge';
8
8
  import { inject, injectable } from 'inversify';
9
9
  import { IOCIdentifier } from '@/core/IOC';
10
- import { LoginInterface } from '@/base/port/LoginInterface';
10
+ import { LoginInterface, RegisterFormData } from '@/base/port/LoginInterface';
11
11
  import { UserApi } from '@/base/apis/userApi/UserApi';
12
12
  import { AppError } from '@/base/cases/AppError';
13
- import { LOCAL_NO_USER_TOKEN } from '@config/Identifier.Error';
14
- import { SliceStore } from '@qlover/slice-store-react';
13
+ import { LOCAL_NO_USER_TOKEN } from '@config/Identifier/Error';
14
+ import { StoreInterface, StoreStateInterface } from '../port/StoreInterface';
15
15
 
16
- class UserControllerState {
16
+ class UserServiceState implements StoreStateInterface {
17
17
  success: boolean = false;
18
18
  userInfo: UserApiGetUserInfoTransaction['response']['data'] = {
19
19
  name: '',
@@ -23,22 +23,22 @@ class UserControllerState {
23
23
  }
24
24
 
25
25
  @injectable()
26
- export class UserController
27
- extends SliceStore<UserControllerState>
26
+ export class UserService
27
+ extends StoreInterface<UserServiceState>
28
28
  implements ExecutorPlugin, LoginInterface
29
29
  {
30
- readonly pluginName = 'UserController';
30
+ readonly pluginName = 'UserService';
31
31
 
32
32
  constructor(
33
33
  @inject(UserApi) private userApi: UserApi,
34
- @inject(RouterController) private routerController: RouterController,
34
+ @inject(RouteService) private routerController: RouteService,
35
35
  @inject(IOCIdentifier.FeApiToken)
36
36
  private userToken: StorageTokenInterface<string>
37
37
  ) {
38
- super(() => new UserControllerState());
38
+ super(() => new UserServiceState());
39
39
  }
40
40
 
41
- setState(state: Partial<UserControllerState>): void {
41
+ setState(state: Partial<UserServiceState>): void {
42
42
  this.emit({ ...this.state, ...state });
43
43
  }
44
44
 
@@ -111,4 +111,14 @@ export class UserController
111
111
  isAuthenticated(): boolean {
112
112
  return this.state.success;
113
113
  }
114
+
115
+ /**
116
+ * @override
117
+ */
118
+ async register(params: RegisterFormData): Promise<unknown> {
119
+ return this.login({
120
+ username: params.username,
121
+ password: params.password
122
+ });
123
+ }
114
124
  }
@@ -4,7 +4,7 @@ import { IOC } from './IOC';
4
4
  import * as globals from '@/core/globals';
5
5
  import { IocRegister } from './registers';
6
6
  import { BootstrapsRegistry } from './bootstraps';
7
- import { GLOBAL_NO_WINDOW } from '@config/Identifier.Error';
7
+ import { GLOBAL_NO_WINDOW } from '@config/Identifier/Error';
8
8
 
9
9
  export default async function startup({
10
10
  root,
@@ -17,8 +17,10 @@ import {
17
17
  import mockDataJson from '@config/feapi.mock.json';
18
18
  import { RequestStatusCatcher } from '@/base/cases/RequestStatusCatcher';
19
19
  import themeConfig from '@config/theme.json';
20
- import { localJsonStorage } from '../globals';
20
+ import { localJsonStorage, logger } from '../globals';
21
21
  import { I18nService } from '@/base/services/I18nService';
22
+ import { RouteService } from '@/base/services/RouteService';
23
+ import { base as baseRoutes } from '@config/app.router.json';
22
24
 
23
25
  export class RegisterCommon implements InversifyRegisterInterface {
24
26
  register(
@@ -64,6 +66,14 @@ export class RegisterCommon implements InversifyRegisterInterface {
64
66
  })
65
67
  );
66
68
 
69
+ container.bind(
70
+ RouteService,
71
+ new RouteService({
72
+ config: baseRoutes,
73
+ logger
74
+ })
75
+ );
76
+
67
77
  container.bind(I18nService, new I18nService(options.pathname));
68
78
  }
69
79
  }
@@ -1,24 +1,15 @@
1
- import { localJsonStorage, logger } from '../globals';
2
- import { RouterController } from '@/uikit/controllers/RouterController';
1
+ import { localJsonStorage } from '../globals';
3
2
  import { JSONStorageController } from '@/uikit/controllers/JSONStorageController';
4
3
  import { ProcesserService } from '@/base/services/ProcesserService';
5
- import { UserController } from '@/uikit/controllers/UserController';
6
- import { base as baseRoutes } from '@config/app.router.json';
4
+ import { UserService } from '@/base/services/UserService';
7
5
  import { InversifyRegisterInterface } from '../IOC';
8
6
  import { InversifyContainer } from '../IOC';
9
-
10
7
  export class RegisterControllers implements InversifyRegisterInterface {
11
8
  register(container: InversifyContainer): void {
12
- const routerController = new RouterController({
13
- config: baseRoutes,
14
- logger
15
- });
16
-
17
9
  const jsonStorageController = new JSONStorageController(localJsonStorage);
18
10
 
19
- container.bind(RouterController, routerController);
20
11
  container.bind(JSONStorageController, jsonStorageController);
21
12
 
22
- container.get(ProcesserService).use(container.get(UserController));
13
+ container.get(ProcesserService).use(container.get(UserService));
23
14
  }
24
15
  }
@@ -1,14 +1,14 @@
1
1
  import { IOC } from '@/core/IOC';
2
- import { UserController } from '@/uikit/controllers/UserController';
2
+ import { UserService } from '@/base/services/UserService';
3
3
  import { Navigate, Outlet } from 'react-router-dom';
4
- import { useSliceStore } from '@qlover/slice-store-react';
4
+ import { useStore } from '@/uikit/hooks/useStore';
5
5
 
6
6
  export default function Layout() {
7
- const userController = IOC(UserController);
8
- useSliceStore(userController, (state) => state.success);
7
+ const userService = IOC(UserService);
8
+ useStore(userService, (state) => state.success);
9
9
 
10
10
  // If user is authenticated, redirect to home page
11
- if (userController.isAuthenticated()) {
11
+ if (userService.isAuthenticated()) {
12
12
  return <Navigate to="/" replace />;
13
13
  }
14
14
 
@@ -3,9 +3,11 @@ import { Form, Input, Button } from 'antd';
3
3
  import { UserOutlined, LockOutlined, GoogleOutlined } from '@ant-design/icons';
4
4
  import { IOC } from '@/core/IOC';
5
5
  import { useBaseRoutePage } from '@/uikit/contexts/BaseRouteContext';
6
- import { RouterController } from '@/uikit/controllers/RouterController';
7
- import { UserController } from '@/uikit/controllers/UserController';
8
- import { useSliceStore } from '@qlover/slice-store-react';
6
+ import { RouteService } from '@/base/services/RouteService';
7
+ import { UserService } from '@/base/services/UserService';
8
+ import { useStore } from '@/uikit/hooks/useStore';
9
+ import * as i18nKeys from '@config/Identifier/I18n';
10
+ import LocaleLink from '@/uikit/components/LocaleLink';
9
11
 
10
12
  interface LoginFormData {
11
13
  email: string;
@@ -14,19 +16,19 @@ interface LoginFormData {
14
16
 
15
17
  export default function Login() {
16
18
  const { t } = useBaseRoutePage();
17
- const userController = IOC(UserController);
19
+ const userService = IOC(UserService);
18
20
  const AppConfig = IOC('AppConfig');
19
- useSliceStore(userController);
21
+ useStore(userService);
20
22
  const [loading, setLoading] = useState(false);
21
23
 
22
24
  const handleLogin = async (values: LoginFormData) => {
23
25
  try {
24
26
  setLoading(true);
25
- await userController.login({
27
+ await userService.login({
26
28
  username: values.email,
27
29
  password: values.password
28
30
  });
29
- IOC(RouterController).replaceToHome();
31
+ IOC(RouteService).replaceToHome();
30
32
  } catch (error) {
31
33
  console.error(error);
32
34
  } finally {
@@ -45,19 +47,21 @@ export default function Login() {
45
47
  </span>
46
48
  </div>
47
49
  <h1 className="text-4xl font-bold text-text mb-4">
48
- Welcome back to the future of learning
50
+ {t(i18nKeys.LOGIN_WELCOME)}
49
51
  </h1>
50
52
  <p className="text-text-secondary text-lg mb-8">
51
- Unlock personalized AI-powered learning experiences designed to
52
- accelerate your knowledge journey.
53
+ {t(i18nKeys.LOGIN_SUBTITLE)}
53
54
  </p>
54
55
  <div className="space-y-4">
56
+ <FeatureItem icon="🎯" text={t(i18nKeys.LOGIN_FEATURE_AI_PATHS)} />
55
57
  <FeatureItem
56
58
  icon="🎯"
57
- text="AI-powered personalized learning paths"
59
+ text={t(i18nKeys.LOGIN_FEATURE_SMART_RECOMMENDATIONS)}
60
+ />
61
+ <FeatureItem
62
+ icon="📊"
63
+ text={t(i18nKeys.LOGIN_FEATURE_PROGRESS_TRACKING)}
58
64
  />
59
- <FeatureItem icon="🎯" text="Smart content recommendations" />
60
- <FeatureItem icon="📊" text="Real-time progress tracking" />
61
65
  </div>
62
66
  </div>
63
67
 
@@ -65,10 +69,10 @@ export default function Login() {
65
69
  <div className="w-full lg:w-1/2 p-8 sm:p-12 flex items-center justify-center">
66
70
  <div className="w-full max-w-[420px]">
67
71
  <h2 className="text-2xl font-semibold mb-2 text-text">
68
- {t('Sign in to your account')}
72
+ {t(i18nKeys.LOGIN_TITLE)}
69
73
  </h2>
70
74
  <p className="text-text-secondary mb-8">
71
- Enter your credentials to access your dashboard
75
+ {t(i18nKeys.LOGIN_SUBTITLE)}
72
76
  </p>
73
77
 
74
78
  <Form
@@ -83,11 +87,14 @@ export default function Login() {
83
87
  >
84
88
  <Form.Item
85
89
  name="email"
86
- rules={[{ required: true, message: 'Please input your email!' }]}
90
+ rules={[
91
+ { required: true, message: t(i18nKeys.LOGIN_EMAIL_REQUIRED) }
92
+ ]}
87
93
  >
88
94
  <Input
89
95
  prefix={<UserOutlined className="text-text-tertiary" />}
90
- placeholder={t('email')}
96
+ placeholder={t(i18nKeys.LOGIN_EMAIL)}
97
+ title={t(i18nKeys.LOGIN_EMAIL_TITLE)}
91
98
  className="h-12 text-base bg-secondary border-border"
92
99
  />
93
100
  </Form.Item>
@@ -95,19 +102,24 @@ export default function Login() {
95
102
  <Form.Item
96
103
  name="password"
97
104
  rules={[
98
- { required: true, message: 'Please input your password!' }
105
+ { required: true, message: t(i18nKeys.LOGIN_PASSWORD_REQUIRED) }
99
106
  ]}
100
107
  >
101
108
  <Input.Password
102
109
  prefix={<LockOutlined />}
103
- placeholder={t('password')}
110
+ placeholder={t(i18nKeys.LOGIN_PASSWORD)}
111
+ title={t(i18nKeys.LOGIN_PASSWORD_TITLE)}
104
112
  className="h-12 text-base"
105
113
  />
106
114
  </Form.Item>
107
115
 
108
116
  <div className="flex justify-end">
109
- <a href="#" className="text-brand hover:text-brand-hover">
110
- {t('Forgot your password?')}
117
+ <a
118
+ href="#"
119
+ className="text-brand hover:text-brand-hover"
120
+ title={t(i18nKeys.LOGIN_FORGOT_PASSWORD_TITLE)}
121
+ >
122
+ {t(i18nKeys.LOGIN_FORGOT_PASSWORD)}
111
123
  </a>
112
124
  </div>
113
125
 
@@ -116,27 +128,36 @@ export default function Login() {
116
128
  type="primary"
117
129
  htmlType="submit"
118
130
  loading={loading}
131
+ title={t(i18nKeys.LOGIN_BUTTON_TITLE)}
119
132
  className="w-full h-12 text-base"
120
133
  >
121
- {t('Sign In')}
134
+ {t(i18nKeys.LOGIN_BUTTON)}
122
135
  </Button>
123
136
  </Form.Item>
124
137
 
125
138
  <div className="text-center text-text-tertiary my-4">
126
- or continue with
139
+ {t(i18nKeys.LOGIN_CONTINUE_WITH)}
127
140
  </div>
128
141
 
129
- <Button icon={<GoogleOutlined />} className="w-full h-12 text-base">
130
- Sign in with Google
142
+ <Button
143
+ icon={<GoogleOutlined />}
144
+ className="w-full h-12 text-base"
145
+ title={t(i18nKeys.LOGIN_WITH_GOOGLE_TITLE)}
146
+ >
147
+ {t(i18nKeys.LOGIN_WITH_GOOGLE)}
131
148
  </Button>
132
149
 
133
150
  <div className="text-center mt-6">
134
151
  <span className="text-text-tertiary">
135
- Don't have an account?{' '}
152
+ {t(i18nKeys.LOGIN_NO_ACCOUNT)}{' '}
136
153
  </span>
137
- <a href="#" className="text-brand hover:text-brand-hover">
138
- Create one here
139
- </a>
154
+ <LocaleLink
155
+ href="/register"
156
+ className="text-brand hover:text-brand-hover"
157
+ title={t(i18nKeys.LOGIN_CREATE_ACCOUNT_TITLE)}
158
+ >
159
+ {t(i18nKeys.LOGIN_CREATE_ACCOUNT)}
160
+ </LocaleLink>
140
161
  </div>
141
162
  </Form>
142
163
  </div>