@qlover/create-app 0.6.3 → 0.7.0
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.
- package/CHANGELOG.md +18 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/react-app/__tests__/__mocks__/I18nService.ts +13 -0
- package/dist/templates/react-app/__tests__/__mocks__/MockAppConfit.ts +48 -0
- package/dist/templates/react-app/__tests__/__mocks__/MockDialogHandler.ts +16 -0
- package/dist/templates/react-app/__tests__/__mocks__/MockLogger.ts +14 -0
- package/dist/templates/react-app/__tests__/__mocks__/createMockGlobals.ts +92 -0
- package/dist/templates/react-app/__tests__/setup/index.ts +51 -0
- package/dist/templates/react-app/__tests__/src/App.test.tsx +139 -0
- package/dist/templates/react-app/__tests__/src/base/cases/AppConfig.test.ts +288 -0
- package/dist/templates/react-app/__tests__/src/base/cases/AppError.test.ts +102 -0
- package/dist/templates/react-app/__tests__/src/base/cases/DialogHandler.test.ts +228 -0
- package/dist/templates/react-app/__tests__/src/base/cases/I18nKeyErrorPlugin.test.ts +207 -0
- package/dist/templates/react-app/__tests__/src/base/cases/InversifyContainer.test.ts +181 -0
- package/dist/templates/react-app/__tests__/src/base/cases/PublicAssetsPath.test.ts +61 -0
- package/dist/templates/react-app/__tests__/src/base/cases/RequestLogger.test.ts +199 -0
- package/dist/templates/react-app/__tests__/src/base/cases/RequestStatusCatcher.test.ts +192 -0
- package/dist/templates/react-app/__tests__/src/base/cases/RouterLoader.test.ts +235 -0
- package/dist/templates/react-app/__tests__/src/base/services/I18nService.test.ts +224 -0
- package/dist/templates/react-app/__tests__/src/core/IOC.test.ts +257 -0
- package/dist/templates/react-app/__tests__/src/core/bootstraps/BootstrapsApp.test.ts +72 -0
- package/dist/templates/react-app/__tests__/src/main.integration.test.tsx +62 -0
- package/dist/templates/react-app/__tests__/src/main.test.tsx +46 -0
- package/dist/templates/react-app/__tests__/src/uikit/components/BaseHeader.test.tsx +88 -0
- package/dist/templates/react-app/config/app.router.ts +155 -0
- package/dist/templates/react-app/config/common.ts +9 -1
- package/dist/templates/react-app/docs/en/test-guide.md +782 -0
- package/dist/templates/react-app/docs/zh/test-guide.md +782 -0
- package/dist/templates/react-app/package.json +8 -19
- package/dist/templates/react-app/src/base/cases/AppConfig.ts +16 -9
- package/dist/templates/react-app/src/base/cases/PublicAssetsPath.ts +7 -1
- package/dist/templates/react-app/src/base/services/I18nService.ts +15 -4
- package/dist/templates/react-app/src/base/services/RouteService.ts +43 -7
- package/dist/templates/react-app/src/core/bootstraps/BootstrapApp.ts +31 -10
- package/dist/templates/react-app/src/core/bootstraps/BootstrapsRegistry.ts +1 -1
- package/dist/templates/react-app/src/core/globals.ts +1 -3
- package/dist/templates/react-app/src/core/registers/RegisterCommon.ts +5 -3
- package/dist/templates/react-app/src/main.tsx +6 -1
- package/dist/templates/react-app/src/pages/404.tsx +0 -1
- package/dist/templates/react-app/src/pages/500.tsx +1 -1
- package/dist/templates/react-app/src/pages/base/RedirectPathname.tsx +3 -1
- package/dist/templates/react-app/src/uikit/components/BaseHeader.tsx +9 -2
- package/dist/templates/react-app/src/uikit/components/LocaleLink.tsx +5 -3
- package/dist/templates/react-app/src/uikit/hooks/useI18nGuard.ts +4 -6
- package/dist/templates/react-app/tsconfig.json +2 -1
- package/dist/templates/react-app/tsconfig.test.json +13 -0
- package/dist/templates/react-app/vite.config.ts +3 -2
- package/package.json +1 -1
|
@@ -7,34 +7,21 @@
|
|
|
7
7
|
"homepage": "",
|
|
8
8
|
"author": "qlover",
|
|
9
9
|
"license": "ISC",
|
|
10
|
-
"main": "./dist/es/index.js",
|
|
11
|
-
"module": "./dist/es/index.js",
|
|
12
|
-
"types": "./dist/es/index.d.ts",
|
|
13
|
-
"exports": {
|
|
14
|
-
".": {
|
|
15
|
-
"types": "./dist/es/index.d.ts",
|
|
16
|
-
"import": "./dist/es/index.js",
|
|
17
|
-
"require": "./dist/cjs/index.js"
|
|
18
|
-
},
|
|
19
|
-
"./cjs/*": "./dist/cjs/*",
|
|
20
|
-
"./es/*": "./dist/es/*",
|
|
21
|
-
"./package.json": "./package.json"
|
|
22
|
-
},
|
|
23
10
|
"repository": {
|
|
24
11
|
"type": "git",
|
|
25
12
|
"url": "git+https://github.com/qlover/fe-base.git",
|
|
26
13
|
"directory": ""
|
|
27
14
|
},
|
|
28
15
|
"files": [
|
|
29
|
-
"bin",
|
|
30
16
|
"dist",
|
|
31
17
|
"package.json",
|
|
32
|
-
"README.md"
|
|
18
|
+
"README.md",
|
|
19
|
+
"README.en.md",
|
|
20
|
+
"docs"
|
|
33
21
|
],
|
|
34
22
|
"keywords": [
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"fe-release"
|
|
23
|
+
"react-app",
|
|
24
|
+
"react-app-template"
|
|
38
25
|
],
|
|
39
26
|
"publishConfig": {
|
|
40
27
|
"access": "public"
|
|
@@ -57,7 +44,7 @@
|
|
|
57
44
|
"@qlover/corekit-bridge": "latest",
|
|
58
45
|
"@qlover/fe-corekit": "latest",
|
|
59
46
|
"@qlover/logger": "^0.1.1",
|
|
60
|
-
"@qlover/slice-store-react": "^1.
|
|
47
|
+
"@qlover/slice-store-react": "^1.2.6",
|
|
61
48
|
"@tailwindcss/postcss": "^4.1.8",
|
|
62
49
|
"@tailwindcss/vite": "^4.1.8",
|
|
63
50
|
"antd": "^5.25.3",
|
|
@@ -80,6 +67,7 @@
|
|
|
80
67
|
"@qlover/eslint-plugin-fe-dev": "^0.2.0",
|
|
81
68
|
"@qlover/fe-scripts": "latest",
|
|
82
69
|
"@qlover/fe-standard": "^0.0.4",
|
|
70
|
+
"@testing-library/react": "^16.3.0",
|
|
83
71
|
"@types/lodash": "^4.17.13",
|
|
84
72
|
"@types/react": "^18.3.11",
|
|
85
73
|
"@types/react-dom": "^18.3.0",
|
|
@@ -95,6 +83,7 @@
|
|
|
95
83
|
"eslint-plugin-react-refresh": "^0.4.14",
|
|
96
84
|
"eslint-plugin-vitest": "^0.5.4",
|
|
97
85
|
"globals": "^15.12.0",
|
|
86
|
+
"jsdom": "^26.1.0",
|
|
98
87
|
"postcss": "^8.5.4",
|
|
99
88
|
"prettier": "^3.5.3",
|
|
100
89
|
"sass-embedded": "^1.79.4",
|
|
@@ -28,6 +28,17 @@ import type { EnvConfigInterface } from '@qlover/corekit-bridge';
|
|
|
28
28
|
* console.log(config.aiApiBaseUrl); // Value from VITE_AI_API_BASE_URL
|
|
29
29
|
*/
|
|
30
30
|
export class AppConfig implements EnvConfigInterface {
|
|
31
|
+
constructor(
|
|
32
|
+
/**
|
|
33
|
+
* Current environment mode for Vite
|
|
34
|
+
* @description Represents the running environment (development, production, etc.)
|
|
35
|
+
* Automatically set based on the current .env file being used
|
|
36
|
+
*
|
|
37
|
+
* from vite.config `VITE_USER_NODE_ENV`
|
|
38
|
+
*/
|
|
39
|
+
readonly env: string = import.meta.env.VITE_USER_NODE_ENV
|
|
40
|
+
) {}
|
|
41
|
+
|
|
31
42
|
/**
|
|
32
43
|
* Application name identifier
|
|
33
44
|
* @description Injected from VITE_APP_NAME environment variable
|
|
@@ -40,15 +51,6 @@ export class AppConfig implements EnvConfigInterface {
|
|
|
40
51
|
*/
|
|
41
52
|
readonly appVersion = '';
|
|
42
53
|
|
|
43
|
-
/**
|
|
44
|
-
* Current environment mode for Vite
|
|
45
|
-
* @description Represents the running environment (development, production, etc.)
|
|
46
|
-
* Automatically set based on the current .env file being used
|
|
47
|
-
*
|
|
48
|
-
* from vite.config `mode`
|
|
49
|
-
*/
|
|
50
|
-
readonly env: string = import.meta.env.MODE;
|
|
51
|
-
|
|
52
54
|
/**
|
|
53
55
|
* Storage key for user authentication token
|
|
54
56
|
* @description Injected from VITE_USER_TOKEN_STORAGE_KEY environment variable
|
|
@@ -113,4 +115,9 @@ export class AppConfig implements EnvConfigInterface {
|
|
|
113
115
|
* Project startup href, usually from window.location.href
|
|
114
116
|
*/
|
|
115
117
|
readonly bootHref = '';
|
|
118
|
+
|
|
119
|
+
/** Flag indicating if the current environment is production */
|
|
120
|
+
get isProduction(): boolean {
|
|
121
|
+
return this.env === 'production';
|
|
122
|
+
}
|
|
116
123
|
}
|
|
@@ -12,6 +12,12 @@ export class PublicAssetsPath {
|
|
|
12
12
|
constructor(protected prefix: string = routerPrefix) {}
|
|
13
13
|
|
|
14
14
|
getPath(path: string): string {
|
|
15
|
-
|
|
15
|
+
if (!this.prefix) {
|
|
16
|
+
return path.startsWith('/') ? path : `/${path}`;
|
|
17
|
+
}
|
|
18
|
+
const prefix = this.prefix.endsWith('/')
|
|
19
|
+
? this.prefix.slice(0, -1)
|
|
20
|
+
: this.prefix;
|
|
21
|
+
return prefix + (path.startsWith('/') ? path : `/${path}`);
|
|
16
22
|
}
|
|
17
23
|
}
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
type StoreStateInterface,
|
|
10
10
|
StoreInterface
|
|
11
11
|
} from '@qlover/corekit-bridge';
|
|
12
|
+
import { useLocaleRoutes } from '@config/common';
|
|
13
|
+
|
|
12
14
|
const { supportedLngs, fallbackLng } = i18nConfig;
|
|
13
15
|
|
|
14
16
|
export type I18nServiceLocale = (typeof supportedLngs)[number];
|
|
@@ -46,8 +48,10 @@ export class I18nService
|
|
|
46
48
|
merge({}, i18nConfig, {
|
|
47
49
|
debug,
|
|
48
50
|
detection: {
|
|
49
|
-
order:
|
|
50
|
-
|
|
51
|
+
order: useLocaleRoutes
|
|
52
|
+
? ['pathLanguageDetector', 'navigator', 'localStorage']
|
|
53
|
+
: ['localStorage', 'navigator'],
|
|
54
|
+
caches: useLocaleRoutes ? [] : ['localStorage']
|
|
51
55
|
}
|
|
52
56
|
})
|
|
53
57
|
);
|
|
@@ -67,8 +71,11 @@ export class I18nService
|
|
|
67
71
|
|
|
68
72
|
return fallbackLng;
|
|
69
73
|
},
|
|
70
|
-
cacheUserLanguage() {
|
|
71
|
-
//
|
|
74
|
+
cacheUserLanguage(lng: string) {
|
|
75
|
+
// Only cache language if not using locale routes
|
|
76
|
+
if (!useLocaleRoutes) {
|
|
77
|
+
localStorage.setItem('i18nextLng', lng);
|
|
78
|
+
}
|
|
72
79
|
}
|
|
73
80
|
};
|
|
74
81
|
i18n.services.languageDetector.addDetector(pathLanguageDetector);
|
|
@@ -76,6 +83,10 @@ export class I18nService
|
|
|
76
83
|
|
|
77
84
|
async changeLanguage(language: I18nServiceLocale): Promise<void> {
|
|
78
85
|
await i18n.changeLanguage(language);
|
|
86
|
+
// 如果不使用本地化路由,则保存语言设置到本地存储
|
|
87
|
+
if (!useLocaleRoutes) {
|
|
88
|
+
localStorage.setItem('i18nextLng', language);
|
|
89
|
+
}
|
|
79
90
|
}
|
|
80
91
|
|
|
81
92
|
changeLoading(loading: boolean): void {
|
|
@@ -14,11 +14,20 @@ export type RouterServiceDependencies = {
|
|
|
14
14
|
|
|
15
15
|
export type RouterServiceOptions = {
|
|
16
16
|
routes: RouteConfigValue[];
|
|
17
|
+
/**
|
|
18
|
+
* Whether to use locale routes
|
|
19
|
+
*
|
|
20
|
+
* @default `false`
|
|
21
|
+
*/
|
|
22
|
+
hasLocalRoutes?: boolean;
|
|
17
23
|
logger: LoggerInterface;
|
|
18
24
|
};
|
|
19
25
|
|
|
20
26
|
export class RouterServiceState implements StoreStateInterface {
|
|
21
|
-
constructor(
|
|
27
|
+
constructor(
|
|
28
|
+
public routes: RouteConfigValue[] = [],
|
|
29
|
+
public localeRoutes: boolean = false
|
|
30
|
+
) {}
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
export class RouteService
|
|
@@ -31,7 +40,9 @@ export class RouteService
|
|
|
31
40
|
dependencies?: RouterServiceDependencies;
|
|
32
41
|
|
|
33
42
|
constructor(private options: RouterServiceOptions) {
|
|
34
|
-
super(
|
|
43
|
+
super(
|
|
44
|
+
() => new RouterServiceState(options.routes, !!options.hasLocalRoutes)
|
|
45
|
+
);
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
get logger(): LoggerInterface {
|
|
@@ -51,8 +62,11 @@ export class RouteService
|
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
composePath(path: string): string {
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
if (this.state.localeRoutes) {
|
|
66
|
+
const targetLang = I18nService.getCurrentLanguage();
|
|
67
|
+
return `/${targetLang}${path}`;
|
|
68
|
+
}
|
|
69
|
+
return path.startsWith('/') ? path : `/${path}`;
|
|
56
70
|
}
|
|
57
71
|
|
|
58
72
|
/**
|
|
@@ -70,13 +84,19 @@ export class RouteService
|
|
|
70
84
|
}
|
|
71
85
|
|
|
72
86
|
changeRoutes(routes: RouteConfigValue[]): void {
|
|
73
|
-
this.emit({ routes });
|
|
87
|
+
this.emit({ routes, localeRoutes: this.state.localeRoutes });
|
|
74
88
|
}
|
|
75
89
|
|
|
76
|
-
goto(
|
|
90
|
+
goto(
|
|
91
|
+
path: string,
|
|
92
|
+
options?: NavigateOptions & {
|
|
93
|
+
navigate?: NavigateFunction;
|
|
94
|
+
}
|
|
95
|
+
): void {
|
|
96
|
+
const { navigate, ...rest } = options || {};
|
|
77
97
|
path = this.composePath(path);
|
|
78
98
|
this.logger.debug('Goto path => ', path);
|
|
79
|
-
this.navigate?.(path,
|
|
99
|
+
(navigate || this.navigate)?.(path, rest);
|
|
80
100
|
}
|
|
81
101
|
|
|
82
102
|
gotoLogin(): void {
|
|
@@ -86,4 +106,20 @@ export class RouteService
|
|
|
86
106
|
replaceToHome(): void {
|
|
87
107
|
this.goto('/', { replace: true });
|
|
88
108
|
}
|
|
109
|
+
|
|
110
|
+
redirectToDefault(navigate: NavigateFunction): void {
|
|
111
|
+
this.goto('/', { replace: true, navigate });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
i18nGuard(lng: string, navigate: NavigateFunction): void {
|
|
115
|
+
if (!this.state.localeRoutes) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!lng) {
|
|
120
|
+
this.goto('/404', { replace: true, navigate });
|
|
121
|
+
} else if (!I18nService.isValidLanguage(lng)) {
|
|
122
|
+
this.goto('/404', { replace: true, navigate });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
89
125
|
}
|
|
@@ -1,34 +1,52 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Bootstrap,
|
|
3
|
+
IOCContainerInterface,
|
|
4
|
+
IOCFunctionInterface
|
|
5
|
+
} from '@qlover/corekit-bridge';
|
|
2
6
|
import { envBlackList, envPrefix, browserGlobalsName } from '@config/common';
|
|
3
|
-
import { IOC } from '../IOC';
|
|
4
7
|
import * as globals from '../globals';
|
|
5
|
-
import { GLOBAL_NO_WINDOW } from '@config/Identifier/common.error';
|
|
6
8
|
import { IocRegisterImpl } from '../registers/IocRegisterImpl';
|
|
7
9
|
import { BootstrapsRegistry } from './BootstrapsRegistry';
|
|
10
|
+
import { isObject } from 'lodash';
|
|
11
|
+
import { IOCIdentifierMap } from '../IOC';
|
|
12
|
+
|
|
13
|
+
export type BootstrapAppArgs = {
|
|
14
|
+
/**
|
|
15
|
+
* 启动的根节点,通常是window
|
|
16
|
+
*/
|
|
17
|
+
root: unknown;
|
|
18
|
+
/**
|
|
19
|
+
* 启动的web地址
|
|
20
|
+
*/
|
|
21
|
+
bootHref: string;
|
|
22
|
+
/**
|
|
23
|
+
* IOC容器
|
|
24
|
+
*/
|
|
25
|
+
IOC: IOCFunctionInterface<IOCIdentifierMap, IOCContainerInterface>;
|
|
26
|
+
};
|
|
8
27
|
|
|
9
28
|
export class BootstrapApp {
|
|
10
|
-
static async main(): Promise<
|
|
11
|
-
const root =
|
|
29
|
+
static async main(args: BootstrapAppArgs): Promise<BootstrapAppArgs> {
|
|
30
|
+
const { root, bootHref, IOC } = args;
|
|
12
31
|
|
|
13
|
-
if (!(
|
|
14
|
-
throw new Error(
|
|
32
|
+
if (!isObject(root)) {
|
|
33
|
+
throw new Error('root is not an object');
|
|
15
34
|
}
|
|
16
35
|
|
|
17
36
|
const { logger, appConfig } = globals;
|
|
18
|
-
const { pathname } = root.location;
|
|
19
37
|
|
|
20
38
|
const bootstrap = new Bootstrap({
|
|
21
39
|
root,
|
|
22
40
|
logger,
|
|
23
41
|
ioc: {
|
|
24
42
|
manager: IOC,
|
|
25
|
-
register: new IocRegisterImpl({ pathname, appConfig })
|
|
43
|
+
register: new IocRegisterImpl({ pathname: bootHref, appConfig })
|
|
26
44
|
},
|
|
27
45
|
envOptions: {
|
|
28
46
|
target: appConfig,
|
|
29
47
|
source: {
|
|
30
48
|
...import.meta.env,
|
|
31
|
-
[envPrefix + 'BOOT_HREF']:
|
|
49
|
+
[envPrefix + 'BOOT_HREF']: bootHref
|
|
32
50
|
},
|
|
33
51
|
prefix: envPrefix,
|
|
34
52
|
blackList: envBlackList
|
|
@@ -49,7 +67,10 @@ export class BootstrapApp {
|
|
|
49
67
|
|
|
50
68
|
await bootstrap.use(bootstrapsRegistry.register()).start();
|
|
51
69
|
} catch (error) {
|
|
70
|
+
console.log(error);
|
|
52
71
|
logger.error(`${appConfig.appName} starup error:`, error);
|
|
53
72
|
}
|
|
73
|
+
|
|
74
|
+
return args;
|
|
54
75
|
}
|
|
55
76
|
}
|
|
@@ -16,8 +16,6 @@ import { DialogHandler } from '@/base/cases/DialogHandler';
|
|
|
16
16
|
import { loggerStyles } from '@config/common';
|
|
17
17
|
import { AppConfig } from '@/base/cases/AppConfig';
|
|
18
18
|
|
|
19
|
-
const isProduction = import.meta.env.VITE_USER_NODE_ENV === 'production';
|
|
20
|
-
|
|
21
19
|
export const appConfig = new AppConfig();
|
|
22
20
|
|
|
23
21
|
export const dialogHandler = new DialogHandler();
|
|
@@ -27,7 +25,7 @@ export const dialogHandler = new DialogHandler();
|
|
|
27
25
|
*/
|
|
28
26
|
export const logger = new Logger({
|
|
29
27
|
handlers: new ConsoleHandler(new ColorFormatter(loggerStyles)),
|
|
30
|
-
silent: isProduction,
|
|
28
|
+
silent: appConfig.isProduction,
|
|
31
29
|
level: 'debug'
|
|
32
30
|
});
|
|
33
31
|
|
|
@@ -16,7 +16,8 @@ import { themeConfig } from '@config/theme';
|
|
|
16
16
|
import { localStorage, logger } from '../globals';
|
|
17
17
|
import { I18nService } from '@/base/services/I18nService';
|
|
18
18
|
import { RouteService } from '@/base/services/RouteService';
|
|
19
|
-
import { baseRoutes } from '@config/app.router';
|
|
19
|
+
import { baseRoutes, baseNoLocaleRoutes } from '@config/app.router';
|
|
20
|
+
import { useLocaleRoutes } from '@config/common';
|
|
20
21
|
import { UserService } from '@/base/services/UserService';
|
|
21
22
|
import { IOCRegister } from '../IOC';
|
|
22
23
|
import { IOCIdentifier } from '@config/IOCIdentifier';
|
|
@@ -62,8 +63,9 @@ export const RegisterCommon: IOCRegister = {
|
|
|
62
63
|
container.bind(
|
|
63
64
|
RouteService,
|
|
64
65
|
new RouteService({
|
|
65
|
-
routes: baseRoutes,
|
|
66
|
-
logger
|
|
66
|
+
routes: useLocaleRoutes ? baseRoutes : baseNoLocaleRoutes,
|
|
67
|
+
logger,
|
|
68
|
+
hasLocalRoutes: useLocaleRoutes
|
|
67
69
|
})
|
|
68
70
|
);
|
|
69
71
|
|
|
@@ -4,8 +4,13 @@ import { StrictMode } from 'react';
|
|
|
4
4
|
import { createRoot } from 'react-dom/client';
|
|
5
5
|
import App from './App.tsx';
|
|
6
6
|
import { BootstrapApp } from './core/bootstraps/BootstrapApp';
|
|
7
|
+
import { IOC } from './core/IOC.ts';
|
|
7
8
|
|
|
8
|
-
BootstrapApp.main(
|
|
9
|
+
BootstrapApp.main({
|
|
10
|
+
root: window,
|
|
11
|
+
bootHref: window.location.href,
|
|
12
|
+
IOC: IOC
|
|
13
|
+
});
|
|
9
14
|
|
|
10
15
|
createRoot(document.getElementById('root')!).render(
|
|
11
16
|
<StrictMode>
|
|
@@ -8,7 +8,6 @@ export default function NotFound({ route }: { route?: string }) {
|
|
|
8
8
|
<div className="flex flex-col justify-center min-h-screen py-6 bg-background sm:py-12">
|
|
9
9
|
<div className="relative py-3 mx-auto sm:max-w-xl">
|
|
10
10
|
<h1 className="text-text text-2xl font-bold text-center">
|
|
11
|
-
404 -
|
|
12
11
|
{route ? `${t(NOT_FOUND_COMPONENT)}: ${route}` : t(PAGE_404_TITLE)}
|
|
13
12
|
</h1>
|
|
14
13
|
</div>
|
|
@@ -7,7 +7,7 @@ export default function NotFound500() {
|
|
|
7
7
|
<div className="flex flex-col justify-center min-h-screen py-6 bg-background sm:py-12">
|
|
8
8
|
<div className="relative py-3 mx-auto sm:max-w-xl">
|
|
9
9
|
<h1 className="text-text text-2xl font-bold text-center">
|
|
10
|
-
|
|
10
|
+
{t(PAGE_500_TITLE)}
|
|
11
11
|
</h1>
|
|
12
12
|
</div>
|
|
13
13
|
</div>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { RouteService } from '@/base/services/RouteService';
|
|
2
|
+
import { IOC } from '@/core/IOC';
|
|
1
3
|
import { useEffect } from 'react';
|
|
2
4
|
import { useNavigate } from 'react-router-dom';
|
|
3
5
|
|
|
@@ -6,7 +8,7 @@ const RedirectToDefault = () => {
|
|
|
6
8
|
|
|
7
9
|
useEffect(() => {
|
|
8
10
|
// Redirect to the default language path
|
|
9
|
-
|
|
11
|
+
IOC(RouteService).redirectToDefault(navigate);
|
|
10
12
|
}, [navigate]);
|
|
11
13
|
|
|
12
14
|
return null;
|
|
@@ -11,7 +11,10 @@ export default function BaseHeader({
|
|
|
11
11
|
showLogoutButton?: boolean;
|
|
12
12
|
}) {
|
|
13
13
|
return (
|
|
14
|
-
<header
|
|
14
|
+
<header
|
|
15
|
+
data-testid="base-header"
|
|
16
|
+
className="h-14 bg-secondary border-b border-border sticky top-0 z-50"
|
|
17
|
+
>
|
|
15
18
|
<div className="flex items-center justify-between h-full px-4 mx-auto max-w-7xl">
|
|
16
19
|
<div className="flex items-center">
|
|
17
20
|
<LocaleLink
|
|
@@ -19,11 +22,15 @@ export default function BaseHeader({
|
|
|
19
22
|
className="flex items-center hover:opacity-80 transition-opacity"
|
|
20
23
|
>
|
|
21
24
|
<img
|
|
25
|
+
data-testid="base-header-logo"
|
|
22
26
|
src={IOC(PublicAssetsPath).getPath('/logo.svg')}
|
|
23
27
|
alt="logo"
|
|
24
28
|
className="h-8 w-auto"
|
|
25
29
|
/>
|
|
26
|
-
<span
|
|
30
|
+
<span
|
|
31
|
+
data-testid="base-header-app-name"
|
|
32
|
+
className="ml-2 text-lg font-semibold text-text"
|
|
33
|
+
>
|
|
27
34
|
{IOC('AppConfig').appName}
|
|
28
35
|
</span>
|
|
29
36
|
</LocaleLink>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LinkProps, Link as RouterLink, To, useParams } from 'react-router-dom';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
+
import { useLocaleRoutes } from '@config/common';
|
|
3
4
|
|
|
4
5
|
interface LocaleLinkProps extends Omit<LinkProps, 'href' | 'to'> {
|
|
5
6
|
href: string | To;
|
|
@@ -21,19 +22,20 @@ const LocaleLink: React.FC<LocaleLinkProps> = ({
|
|
|
21
22
|
locale = locale || lng;
|
|
22
23
|
|
|
23
24
|
const isDefaultLocale = locale === defaultLocale;
|
|
25
|
+
const shouldAddLocale = useLocaleRoutes && !isDefaultLocale;
|
|
24
26
|
|
|
25
27
|
let localizedHref: string | To;
|
|
26
28
|
if (typeof href === 'string') {
|
|
27
|
-
localizedHref =
|
|
29
|
+
localizedHref = shouldAddLocale ? `/${locale}${href}` : href;
|
|
28
30
|
} else {
|
|
29
31
|
localizedHref = {
|
|
30
32
|
...href,
|
|
31
|
-
pathname:
|
|
33
|
+
pathname: shouldAddLocale ? `/${locale}${href.pathname}` : href.pathname
|
|
32
34
|
};
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
return (
|
|
36
|
-
<RouterLink {...props} to={localizedHref}>
|
|
38
|
+
<RouterLink data-testid="locale-link" {...props} to={localizedHref}>
|
|
37
39
|
{children}
|
|
38
40
|
</RouterLink>
|
|
39
41
|
);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { I18nServiceLocale } from '@/base/services/I18nService';
|
|
2
|
+
import { RouteService } from '@/base/services/RouteService';
|
|
3
|
+
import { IOC } from '@/core/IOC';
|
|
2
4
|
import { useEffect } from 'react';
|
|
3
5
|
import { useNavigate } from 'react-router-dom';
|
|
4
6
|
import { useParams } from 'react-router-dom';
|
|
@@ -15,10 +17,6 @@ export function useI18nGuard() {
|
|
|
15
17
|
const navigate = useNavigate();
|
|
16
18
|
|
|
17
19
|
useEffect(() => {
|
|
18
|
-
|
|
19
|
-
navigate('/404', { replace: true });
|
|
20
|
-
} else if (!I18nService.isValidLanguage(lng)) {
|
|
21
|
-
navigate('/404', { replace: true });
|
|
22
|
-
}
|
|
20
|
+
IOC(RouteService).i18nGuard(lng as I18nServiceLocale, navigate);
|
|
23
21
|
}, [lng, navigate]);
|
|
24
22
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.app.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"jsx": "react-jsx",
|
|
5
|
+
"types": ["vitest/globals", "vite/client"],
|
|
6
|
+
"paths": {
|
|
7
|
+
"@/*": ["./src/*"],
|
|
8
|
+
"@config/*": ["./config/*"]
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"include": ["__tests__/**/*"],
|
|
12
|
+
"exclude": ["node_modules", "dist"]
|
|
13
|
+
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from './config/common';
|
|
9
9
|
import { name, version } from './package.json';
|
|
10
10
|
import tsconfigPaths from 'vite-tsconfig-paths';
|
|
11
|
-
import envConfig from '@qlover/corekit-bridge/vite-env-config';
|
|
11
|
+
import envConfig from '@qlover/corekit-bridge/build/vite-env-config';
|
|
12
12
|
import ts2Locales from '@brain-toolkit/ts2locales/vite';
|
|
13
13
|
import i18nConfig from './config/i18n';
|
|
14
14
|
import tailwindcss from '@tailwindcss/vite';
|
|
@@ -120,8 +120,9 @@ export default defineConfig({
|
|
|
120
120
|
port: Number(process.env.VITE_SERVER_PORT || 3200)
|
|
121
121
|
},
|
|
122
122
|
test: {
|
|
123
|
+
watch: false,
|
|
123
124
|
environment: 'jsdom',
|
|
124
125
|
globals: true,
|
|
125
|
-
setupFiles: ['./
|
|
126
|
+
setupFiles: ['./__tests__/setup/index.ts']
|
|
126
127
|
}
|
|
127
128
|
});
|