@qlover/create-app 0.7.8 → 0.7.10
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 +140 -0
- package/dist/index.cjs +3 -3
- package/dist/index.js +3 -3
- package/dist/templates/next-app/.env.template +8 -4
- package/dist/templates/next-app/config/IOCIdentifier.ts +4 -1
- package/dist/templates/next-app/config/Identifier/api.ts +20 -0
- package/dist/templates/next-app/config/Identifier/index.ts +2 -0
- package/dist/templates/next-app/config/Identifier/page.home.ts +7 -0
- package/dist/templates/next-app/config/Identifier/page.login.ts +2 -2
- package/dist/templates/next-app/config/Identifier/page.register.ts +43 -22
- package/dist/templates/next-app/config/Identifier/validator.ts +34 -0
- package/dist/templates/next-app/config/i18n/HomeI18n .ts +22 -0
- package/dist/templates/next-app/config/i18n/index.ts +1 -0
- package/dist/templates/next-app/config/i18n/register18n.ts +44 -0
- package/dist/templates/next-app/config/theme.ts +1 -0
- package/dist/templates/next-app/migrations/schema/UserSchema.ts +13 -0
- package/dist/templates/next-app/migrations/sql/1694244000000.sql +10 -0
- package/dist/templates/next-app/package.json +15 -6
- package/dist/templates/next-app/public/locales/en.json +17 -2
- package/dist/templates/next-app/public/locales/zh.json +17 -2
- package/dist/templates/next-app/src/app/[locale]/admin/layout.tsx +21 -0
- package/dist/templates/next-app/src/app/[locale]/admin/page.tsx +10 -0
- package/dist/templates/next-app/src/app/[locale]/layout.tsx +1 -7
- package/dist/templates/next-app/src/app/[locale]/login/LoginForm.tsx +28 -16
- package/dist/templates/next-app/src/app/[locale]/login/page.tsx +10 -17
- package/dist/templates/next-app/src/app/[locale]/page.tsx +94 -102
- package/dist/templates/next-app/src/app/[locale]/register/RegisterForm.tsx +176 -0
- package/dist/templates/next-app/src/app/[locale]/register/page.tsx +79 -0
- package/dist/templates/next-app/src/app/api/user/login/route.ts +50 -0
- package/dist/templates/next-app/src/app/api/user/logout/route.ts +27 -0
- package/dist/templates/next-app/src/app/api/user/register/route.ts +50 -0
- package/dist/templates/next-app/src/base/cases/AppConfig.ts +19 -0
- package/dist/templates/next-app/src/base/cases/DialogErrorPlugin.ts +35 -0
- package/dist/templates/next-app/src/base/cases/RequestEncryptPlugin.ts +70 -0
- package/dist/templates/next-app/src/base/cases/RouterService.ts +4 -0
- package/dist/templates/next-app/src/base/cases/StringEncryptor.ts +67 -0
- package/dist/templates/next-app/src/base/cases/UserServiceApi.ts +48 -0
- package/dist/templates/next-app/src/base/port/AppApiInterface.ts +14 -0
- package/dist/templates/next-app/src/base/port/AppUserApiInterface.ts +15 -0
- package/dist/templates/next-app/src/base/port/DBBridgeInterface.ts +18 -0
- package/dist/templates/next-app/src/base/port/DBMigrationInterface.ts +92 -0
- package/dist/templates/next-app/src/base/port/DBTableInterface.ts +3 -0
- package/dist/templates/next-app/src/base/port/I18nServiceInterface.ts +3 -2
- package/dist/templates/next-app/src/base/port/MigrationApiInterface.ts +3 -0
- package/dist/templates/next-app/src/base/port/ServerApiResponseInterface.ts +6 -0
- package/dist/templates/next-app/src/base/port/UserServiceInterface.ts +3 -2
- package/dist/templates/next-app/src/base/services/I18nService.ts +9 -45
- package/dist/templates/next-app/src/base/services/UserService.ts +9 -8
- package/dist/templates/next-app/src/base/services/appApi/AppApiPlugin.ts +63 -0
- package/dist/templates/next-app/src/base/services/appApi/AppUserApi.ts +72 -0
- package/dist/templates/next-app/src/base/services/appApi/AppUserApiBootstrap.ts +48 -0
- package/dist/templates/next-app/src/base/services/appApi/AppUserType.ts +51 -0
- package/dist/templates/next-app/src/base/services/migrations/MigrationsApi.ts +43 -0
- package/dist/templates/next-app/src/core/bootstraps/BootstrapServer.ts +30 -5
- package/dist/templates/next-app/src/core/bootstraps/BootstrapsRegistry.ts +4 -3
- package/dist/templates/next-app/src/server/AppErrorApi.ts +10 -0
- package/dist/templates/next-app/src/server/AppSuccessApi.ts +7 -0
- package/dist/templates/next-app/src/server/PasswordEncrypt.ts +12 -0
- package/dist/templates/next-app/src/server/ServerAuth.ts +50 -0
- package/dist/templates/next-app/src/server/SupabaseBridge.ts +124 -0
- package/dist/templates/next-app/src/server/UserCredentialToken.ts +49 -0
- package/dist/templates/next-app/src/server/port/CrentialTokenInterface.ts +5 -0
- package/dist/templates/next-app/src/server/port/ServerInterface.ts +22 -0
- package/dist/templates/next-app/src/server/port/UserAuthInterface.ts +9 -0
- package/dist/templates/next-app/src/server/port/UserRepositoryInterface.ts +15 -0
- package/dist/templates/next-app/src/server/port/UserServiceInterface.ts +8 -0
- package/dist/templates/next-app/src/server/port/ValidatorInterface.ts +23 -0
- package/dist/templates/next-app/src/server/repositorys/UserRepository.ts +63 -0
- package/dist/templates/next-app/src/server/services/UserService.ts +105 -0
- package/dist/templates/next-app/src/server/validators/LoginValidator.ts +79 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/_default.css +12 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/dark.css +26 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/pink.css +16 -0
- package/dist/templates/next-app/src/styles/css/page.css +4 -3
- package/dist/templates/next-app/src/styles/css/themes/_default.css +1 -0
- package/dist/templates/next-app/src/styles/css/themes/dark.css +1 -0
- package/dist/templates/next-app/src/styles/css/themes/pink.css +1 -0
- package/dist/templates/next-app/src/uikit/components/BaseHeader.tsx +6 -11
- package/dist/templates/next-app/src/uikit/components/BaseLayout.tsx +27 -0
- package/dist/templates/next-app/src/uikit/components/BootstrapsProvider.tsx +8 -1
- package/dist/templates/next-app/src/uikit/components/LanguageSwitcher.tsx +49 -21
- package/dist/templates/next-app/src/uikit/components/LogoutButton.tsx +20 -10
- package/dist/templates/next-app/src/uikit/components/ThemeSwitcher.tsx +92 -48
- package/dist/templates/next-app/tsconfig.json +3 -1
- package/package.json +2 -2
- package/dist/templates/next-app/src/base/cases/ServerAuth.ts +0 -17
- package/dist/templates/next-app/src/base/port/ServerAuthInterface.ts +0 -3
- package/dist/templates/next-app/src/base/port/ServerInterface.ts +0 -12
- /package/dist/templates/next-app/src/{app/[locale]/login → uikit/components}/FeatureItem.tsx +0 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { inject, injectable } from 'inversify';
|
|
2
|
+
import { isEmpty, last, omit } from 'lodash';
|
|
3
|
+
import {
|
|
4
|
+
API_USER_NOT_FOUND,
|
|
5
|
+
API_USER_ALREADY_EXISTS
|
|
6
|
+
} from '@config/Identifier/api';
|
|
7
|
+
import { PasswordEncrypt } from '../PasswordEncrypt';
|
|
8
|
+
import { UserRepository } from '../repositorys/UserRepository';
|
|
9
|
+
import { ServerAuth } from '../ServerAuth';
|
|
10
|
+
import {
|
|
11
|
+
UserCredentialToken,
|
|
12
|
+
type UserCredentialTokenValue
|
|
13
|
+
} from '../UserCredentialToken';
|
|
14
|
+
import type { CrentialTokenInterface } from '../port/CrentialTokenInterface';
|
|
15
|
+
import type { UserAuthInterface } from '../port/UserAuthInterface';
|
|
16
|
+
import type { UserRepositoryInterface } from '../port/UserRepositoryInterface';
|
|
17
|
+
import type { UserServiceInterface } from '../port/UserServiceInterface';
|
|
18
|
+
import type { UserSchema } from '@migrations/schema/UserSchema';
|
|
19
|
+
import type { Encryptor } from '@qlover/fe-corekit';
|
|
20
|
+
|
|
21
|
+
@injectable()
|
|
22
|
+
export class UserService implements UserServiceInterface {
|
|
23
|
+
constructor(
|
|
24
|
+
@inject(UserRepository)
|
|
25
|
+
protected userRepository: UserRepositoryInterface,
|
|
26
|
+
@inject(ServerAuth)
|
|
27
|
+
protected userAuth: UserAuthInterface,
|
|
28
|
+
@inject(PasswordEncrypt)
|
|
29
|
+
protected encryptor: Encryptor<string, string>,
|
|
30
|
+
@inject(UserCredentialToken)
|
|
31
|
+
protected credentialToken: CrentialTokenInterface<UserCredentialTokenValue>
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
async register(params: {
|
|
35
|
+
email: string;
|
|
36
|
+
password: string;
|
|
37
|
+
}): Promise<UserSchema> {
|
|
38
|
+
const user = await this.userRepository.getUserByEmail(params.email);
|
|
39
|
+
|
|
40
|
+
if (!isEmpty(user)) {
|
|
41
|
+
throw new Error(API_USER_ALREADY_EXISTS);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result = await this.userRepository.add({
|
|
45
|
+
email: params.email,
|
|
46
|
+
password: this.encryptor.encrypt(params.password)
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const target = last(result);
|
|
50
|
+
if (!target) {
|
|
51
|
+
throw new Error(API_USER_NOT_FOUND);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return omit(target, 'password') as UserSchema;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async login(params: {
|
|
58
|
+
email: string;
|
|
59
|
+
password: string;
|
|
60
|
+
}): Promise<UserSchema> {
|
|
61
|
+
const user = await this.userRepository.getUserByEmail(params.email);
|
|
62
|
+
|
|
63
|
+
if (!user) {
|
|
64
|
+
throw new Error(API_USER_NOT_FOUND);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const encryptedPassword = this.encryptor.encrypt(params.password);
|
|
68
|
+
|
|
69
|
+
if (encryptedPassword !== user.password) {
|
|
70
|
+
throw new Error(API_USER_NOT_FOUND);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const credentialToken = await this.credentialToken.generateToken(user);
|
|
74
|
+
|
|
75
|
+
await this.userRepository.updateById(user.id, {
|
|
76
|
+
credential_token: credentialToken
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return Object.assign(omit(user, 'password') as UserSchema, {
|
|
80
|
+
credential_token: credentialToken
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async logout(): Promise<void> {
|
|
85
|
+
const auth = await this.userAuth.getAuth();
|
|
86
|
+
|
|
87
|
+
if (!auth) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const user = await this.credentialToken.parseToken(auth);
|
|
93
|
+
|
|
94
|
+
console.log('user', user);
|
|
95
|
+
|
|
96
|
+
await this.userRepository.updateById(user.id, {
|
|
97
|
+
credential_token: ''
|
|
98
|
+
});
|
|
99
|
+
} catch {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await this.userAuth.clear();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ExecutorError } from '@qlover/fe-corekit';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import {
|
|
4
|
+
V_LOGIN_PARAMS_REQUIRED,
|
|
5
|
+
V_EMAIL_INVALID,
|
|
6
|
+
V_PASSWORD_MIN_LENGTH,
|
|
7
|
+
V_PASSWORD_MAX_LENGTH,
|
|
8
|
+
V_PASSWORD_SPECIAL_CHARS
|
|
9
|
+
} from '@config/Identifier/validator';
|
|
10
|
+
import type {
|
|
11
|
+
ValidatorInterface,
|
|
12
|
+
ValidationFaildResult
|
|
13
|
+
} from '../port/ValidatorInterface';
|
|
14
|
+
|
|
15
|
+
export interface LoginValidatorData {
|
|
16
|
+
email: string;
|
|
17
|
+
password: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const emailSchema = z.email({ error: V_EMAIL_INVALID });
|
|
21
|
+
|
|
22
|
+
const passwordSchema = z
|
|
23
|
+
.string()
|
|
24
|
+
.min(6, { error: V_PASSWORD_MIN_LENGTH })
|
|
25
|
+
.max(50, { error: V_PASSWORD_MAX_LENGTH })
|
|
26
|
+
.regex(/^\S+$/, { error: V_PASSWORD_SPECIAL_CHARS });
|
|
27
|
+
|
|
28
|
+
interface ExtendedExecutorError extends ExecutorError {
|
|
29
|
+
issues?: ValidationFaildResult[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class LoginValidator implements ValidatorInterface {
|
|
33
|
+
validateEmail(data: unknown): void | ValidationFaildResult {
|
|
34
|
+
const emailResult = emailSchema.safeParse(data);
|
|
35
|
+
if (!emailResult.success) {
|
|
36
|
+
return emailResult.error.issues[0];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
validatePassword(data: unknown): void | ValidationFaildResult {
|
|
41
|
+
const passwordResult = passwordSchema.safeParse(data);
|
|
42
|
+
if (!passwordResult.success) {
|
|
43
|
+
return passwordResult.error.issues[0];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
validate(data: unknown): void | ValidationFaildResult {
|
|
48
|
+
if (typeof data !== 'object' || data === null) {
|
|
49
|
+
return {
|
|
50
|
+
path: ['form'],
|
|
51
|
+
message: V_LOGIN_PARAMS_REQUIRED
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { email, password } = data as Record<string, unknown>;
|
|
56
|
+
|
|
57
|
+
let validateResult = this.validateEmail(email);
|
|
58
|
+
if (validateResult != null) {
|
|
59
|
+
return validateResult;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
validateResult = this.validatePassword(password);
|
|
63
|
+
if (validateResult != null) {
|
|
64
|
+
return validateResult;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getThrow(data: unknown): LoginValidatorData {
|
|
69
|
+
const result = this.validate(data);
|
|
70
|
+
|
|
71
|
+
if (result == null) {
|
|
72
|
+
return data as LoginValidatorData;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const error: ExtendedExecutorError = new ExecutorError(result.message);
|
|
76
|
+
error.issues = [result];
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -236,4 +236,16 @@ html,
|
|
|
236
236
|
--fe-message-info-color: var(--fe-color-primary);
|
|
237
237
|
--fe-message-loading-color: var(--fe-color-primary);
|
|
238
238
|
}
|
|
239
|
+
|
|
240
|
+
.ant-dropdown-css-var {
|
|
241
|
+
/* Control Variables - Default Theme */
|
|
242
|
+
--fe-control-outline-width: 2px;
|
|
243
|
+
--fe-control-interactive-size: 16px;
|
|
244
|
+
--fe-control-item-bg-hover: rgba(0, 0, 0, 0.04);
|
|
245
|
+
--fe-control-item-bg-active: #e6f4ff;
|
|
246
|
+
--fe-control-item-bg-active-hover: #bae0ff;
|
|
247
|
+
--fe-control-item-bg-active-disabled: rgba(0, 0, 0, 0.15);
|
|
248
|
+
--fe-control-tmp-outline: rgba(0, 0, 0, 0.02);
|
|
249
|
+
--fe-control-outline: rgba(96, 165, 250, 0.1); /* blue-400 with 0.1 opacity */
|
|
250
|
+
}
|
|
239
251
|
}
|
|
@@ -175,4 +175,30 @@
|
|
|
175
175
|
0.85
|
|
176
176
|
); /* 确保文字在深色背景上清晰可见 */
|
|
177
177
|
}
|
|
178
|
+
.ant-dropdown-css-var {
|
|
179
|
+
/* Control Variables - Dark Theme */
|
|
180
|
+
--fe-control-outline-width: 2px;
|
|
181
|
+
--fe-control-interactive-size: 16px;
|
|
182
|
+
--fe-control-item-bg-hover: rgba(255, 255, 255, 0.08);
|
|
183
|
+
--fe-control-item-bg-active: rgba(
|
|
184
|
+
96,
|
|
185
|
+
165,
|
|
186
|
+
250,
|
|
187
|
+
0.2
|
|
188
|
+
); /* blue-400 with 0.2 opacity */
|
|
189
|
+
--fe-control-item-bg-active-hover: rgba(
|
|
190
|
+
96,
|
|
191
|
+
165,
|
|
192
|
+
250,
|
|
193
|
+
0.3
|
|
194
|
+
); /* blue-400 with 0.3 opacity */
|
|
195
|
+
--fe-control-item-bg-active-disabled: rgba(255, 255, 255, 0.15);
|
|
196
|
+
--fe-control-tmp-outline: rgba(255, 255, 255, 0.02);
|
|
197
|
+
--fe-control-outline: rgba(
|
|
198
|
+
96,
|
|
199
|
+
165,
|
|
200
|
+
250,
|
|
201
|
+
0.1
|
|
202
|
+
); /* blue-400 with 0.1 opacity */
|
|
203
|
+
}
|
|
178
204
|
}
|
|
@@ -201,4 +201,20 @@
|
|
|
201
201
|
0 9px 28px 8px rgba(244, 114, 182, 0.05);
|
|
202
202
|
--fe-modal-mask-bg: rgba(244, 114, 182, 0.45);
|
|
203
203
|
}
|
|
204
|
+
.ant-dropdown-css-var {
|
|
205
|
+
/* Control Variables - Pink Theme */
|
|
206
|
+
--fe-control-outline-width: 2px;
|
|
207
|
+
--fe-control-interactive-size: 16px;
|
|
208
|
+
--fe-control-item-bg-hover: rgba(0, 0, 0, 0.04);
|
|
209
|
+
--fe-control-item-bg-active: #fce7f3; /* pink-100 */
|
|
210
|
+
--fe-control-item-bg-active-hover: #fbcfe8; /* pink-200 */
|
|
211
|
+
--fe-control-item-bg-active-disabled: rgba(0, 0, 0, 0.15);
|
|
212
|
+
--fe-control-tmp-outline: rgba(0, 0, 0, 0.02);
|
|
213
|
+
--fe-control-outline: rgba(
|
|
214
|
+
244,
|
|
215
|
+
114,
|
|
216
|
+
182,
|
|
217
|
+
0.1
|
|
218
|
+
); /* pink-400 with 0.1 opacity */
|
|
219
|
+
}
|
|
204
220
|
}
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
--color-secondary: rgba(var(--color-bg-secondary));
|
|
12
12
|
--color-elevated: rgba(var(--color-bg-elevated));
|
|
13
13
|
--color-text: rgba(var(--text-primary));
|
|
14
|
+
--color-text-hover: rgba(var(--text-primary-hover));
|
|
14
15
|
--color-text-secondary: rgba(var(--text-secondary));
|
|
15
16
|
--color-text-tertiary: rgba(var(--text-tertiary));
|
|
16
|
-
--color-border: rgba(var(--color-border));
|
|
17
|
-
--color-brand: rgba(var(--color-brand));
|
|
18
|
-
--color-brand-hover: rgba(var(--color-brand-hover));
|
|
17
|
+
--color-c-border: rgba(var(--color-border));
|
|
18
|
+
--color-c-brand: rgba(var(--color-brand));
|
|
19
|
+
--color-c-brand-hover: rgba(var(--color-brand-hover));
|
|
19
20
|
}
|
|
@@ -10,10 +10,12 @@ import { ThemeSwitcher } from './ThemeSwitcher';
|
|
|
10
10
|
export function BaseHeader(props: { showLogoutButton?: boolean }) {
|
|
11
11
|
const { showLogoutButton } = props;
|
|
12
12
|
const appConfig = useIOC(IOCIdentifier.AppConfig);
|
|
13
|
+
const i18nService = useIOC(IOCIdentifier.I18nServiceInterface);
|
|
14
|
+
|
|
13
15
|
return (
|
|
14
16
|
<header
|
|
15
|
-
data-testid="
|
|
16
|
-
className="h-14 bg-secondary border-b border-border sticky top-0 z-50"
|
|
17
|
+
data-testid="BaseHeader"
|
|
18
|
+
className="h-14 bg-secondary border-b border-c-border sticky top-0 z-50"
|
|
17
19
|
>
|
|
18
20
|
<div className="flex items-center justify-between h-full px-4 mx-auto max-w-7xl">
|
|
19
21
|
<div className="flex items-center">
|
|
@@ -21,12 +23,6 @@ export function BaseHeader(props: { showLogoutButton?: boolean }) {
|
|
|
21
23
|
href="/"
|
|
22
24
|
className="flex items-center hover:opacity-80 transition-opacity"
|
|
23
25
|
>
|
|
24
|
-
{/* <img
|
|
25
|
-
data-testid="base-header-logo"
|
|
26
|
-
src={IOC(PublicAssetsPath).getPath('/logo.svg')}
|
|
27
|
-
alt="logo"
|
|
28
|
-
className="h-8 w-auto"
|
|
29
|
-
/> */}
|
|
30
26
|
<span
|
|
31
27
|
data-testid="base-header-app-name"
|
|
32
28
|
className="ml-2 text-lg font-semibold text-text"
|
|
@@ -35,10 +31,9 @@ export function BaseHeader(props: { showLogoutButton?: boolean }) {
|
|
|
35
31
|
</span>
|
|
36
32
|
</Link>
|
|
37
33
|
</div>
|
|
38
|
-
<div className="flex items-center gap-4">
|
|
39
|
-
<LanguageSwitcher />
|
|
34
|
+
<div className="flex items-center gap-2 md:gap-4">
|
|
35
|
+
<LanguageSwitcher i18nService={i18nService} />
|
|
40
36
|
<ThemeSwitcher />
|
|
41
|
-
|
|
42
37
|
{showLogoutButton && <LogoutButton />}
|
|
43
38
|
</div>
|
|
44
39
|
</div>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { BaseHeader } from './BaseHeader';
|
|
2
|
+
import type { HTMLAttributes } from 'react';
|
|
3
|
+
|
|
4
|
+
export interface BaseLayoutProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
showLogoutButton?: boolean;
|
|
6
|
+
mainProps?: HTMLAttributes<HTMLElement>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function BaseLayout({
|
|
10
|
+
children,
|
|
11
|
+
showLogoutButton,
|
|
12
|
+
mainProps,
|
|
13
|
+
...props
|
|
14
|
+
}: BaseLayoutProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
data-testid="BaseLayout"
|
|
18
|
+
className="flex flex-col min-h-screen"
|
|
19
|
+
{...props}
|
|
20
|
+
>
|
|
21
|
+
<BaseHeader showLogoutButton={showLogoutButton} />
|
|
22
|
+
<main className="flex flex-1 flex-col bg-primary" {...mainProps}>
|
|
23
|
+
{children}
|
|
24
|
+
</main>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import '@ant-design/v5-patch-for-react-19';
|
|
3
3
|
import { useRouter } from 'next/navigation';
|
|
4
|
-
import { useLocale } from 'next-intl';
|
|
4
|
+
import { useLocale, useTranslations } from 'next-intl';
|
|
5
5
|
import { useEffect } from 'react';
|
|
6
6
|
import { I } from '@config/IOCIdentifier';
|
|
7
7
|
import { NavigateBridge } from '@/base/cases/NavigateBridge';
|
|
8
|
+
import type { I18nServiceLocale } from '@/base/port/I18nServiceInterface';
|
|
8
9
|
import { BootstrapClient } from '@/core/bootstraps/BootstrapClient';
|
|
9
10
|
import { clientIOC } from '@/core/clientIoc/ClientIOC';
|
|
10
11
|
import { IOCContext } from '../context/IOCContext';
|
|
@@ -13,12 +14,18 @@ export function BootstrapsProvider(props: { children: React.ReactNode }) {
|
|
|
13
14
|
const IOC = clientIOC.create();
|
|
14
15
|
const locale = useLocale();
|
|
15
16
|
const router = useRouter();
|
|
17
|
+
const t = useTranslations();
|
|
16
18
|
|
|
17
19
|
useEffect(() => {
|
|
18
20
|
IOC(I.RouterServiceInterface).setLocale(locale);
|
|
19
21
|
IOC(NavigateBridge).setUIBridge(router);
|
|
20
22
|
}, [locale, router, IOC]);
|
|
21
23
|
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
IOC(I.I18nServiceInterface).changeLanguage(locale as I18nServiceLocale);
|
|
26
|
+
IOC(I.I18nServiceInterface).setTranslator(t);
|
|
27
|
+
}, [t, IOC, locale]);
|
|
28
|
+
|
|
22
29
|
useEffect(() => {
|
|
23
30
|
if (typeof window !== 'undefined') {
|
|
24
31
|
BootstrapClient.main({
|
|
@@ -1,24 +1,38 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { TranslationOutlined } from '@ant-design/icons';
|
|
4
|
+
import { Dropdown } from 'antd';
|
|
4
5
|
import { useLocale } from 'next-intl';
|
|
5
|
-
import { useCallback } from 'react';
|
|
6
|
+
import { useCallback, useMemo } from 'react';
|
|
6
7
|
import { i18nConfig } from '@config/i18n';
|
|
7
|
-
import {
|
|
8
|
-
|
|
8
|
+
import type {
|
|
9
|
+
I18nServiceInterface,
|
|
10
|
+
I18nServiceLocale
|
|
11
|
+
} from '@/base/port/I18nServiceInterface';
|
|
9
12
|
import { usePathname, useRouter } from '@/i18n/routing';
|
|
10
|
-
import { useIOC } from '../hook/useIOC';
|
|
11
|
-
import { useStore } from '../hook/useStore';
|
|
12
13
|
import type { LocaleType } from '@config/i18n';
|
|
14
|
+
import type { ItemType } from 'antd/es/menu/interface';
|
|
13
15
|
|
|
14
|
-
export function LanguageSwitcher() {
|
|
15
|
-
const i18nService =
|
|
16
|
-
const { loading } = useStore(i18nService);
|
|
16
|
+
export function LanguageSwitcher(props: { i18nService: I18nServiceInterface }) {
|
|
17
|
+
const { i18nService } = props;
|
|
17
18
|
const pathname = usePathname(); // current pathname, aware of i18n
|
|
18
19
|
|
|
19
20
|
const router = useRouter(); // i18n-aware router instance
|
|
20
21
|
const currentLocale = useLocale() as LocaleType; // currently active locale
|
|
21
22
|
|
|
23
|
+
const options: ItemType[] = useMemo(() => {
|
|
24
|
+
return i18nConfig.supportedLngs.map(
|
|
25
|
+
(lang) =>
|
|
26
|
+
({
|
|
27
|
+
type: 'item',
|
|
28
|
+
key: lang,
|
|
29
|
+
value: lang,
|
|
30
|
+
label:
|
|
31
|
+
i18nConfig.localeNames[lang as keyof typeof i18nConfig.localeNames]
|
|
32
|
+
}) as ItemType
|
|
33
|
+
);
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
22
36
|
const handleLanguageChange = useCallback(
|
|
23
37
|
async (value: string) => {
|
|
24
38
|
// Set a persistent cookie with the user's preferred locale (valid for 1 year)
|
|
@@ -35,18 +49,32 @@ export function LanguageSwitcher() {
|
|
|
35
49
|
[i18nService, pathname, router]
|
|
36
50
|
);
|
|
37
51
|
|
|
52
|
+
const nextLocale = useMemo(() => {
|
|
53
|
+
const targetIndex = i18nConfig.supportedLngs.indexOf(currentLocale) + 1;
|
|
54
|
+
return i18nConfig.supportedLngs[
|
|
55
|
+
targetIndex % i18nConfig.supportedLngs.length
|
|
56
|
+
];
|
|
57
|
+
}, [currentLocale]);
|
|
58
|
+
|
|
38
59
|
return (
|
|
39
|
-
<
|
|
40
|
-
data-testid="
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
<Dropdown
|
|
61
|
+
data-testid="LanguageSwitcherDropdown"
|
|
62
|
+
trigger={['hover']}
|
|
63
|
+
menu={{
|
|
64
|
+
selectedKeys: [currentLocale],
|
|
65
|
+
items: options,
|
|
66
|
+
onClick: ({ key }) => {
|
|
67
|
+
handleLanguageChange(key);
|
|
68
|
+
}
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<span
|
|
72
|
+
data-testid="LanguageSwitcher"
|
|
73
|
+
className="text-text hover:text-text-hover cursor-pointer text-lg transition-colors"
|
|
74
|
+
onClick={() => handleLanguageChange(nextLocale)}
|
|
75
|
+
>
|
|
76
|
+
<TranslationOutlined />
|
|
77
|
+
</span>
|
|
78
|
+
</Dropdown>
|
|
51
79
|
);
|
|
52
80
|
}
|
|
@@ -1,34 +1,44 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LogoutOutlined } from '@ant-design/icons';
|
|
2
|
+
import { Tooltip } from 'antd';
|
|
2
3
|
import { useCallback } from 'react';
|
|
3
4
|
import {
|
|
4
5
|
AUTH_LOGOUT_DIALOG_CONTENT,
|
|
5
6
|
AUTH_LOGOUT_DIALOG_TITLE
|
|
6
7
|
} from '@config/Identifier';
|
|
7
|
-
import {
|
|
8
|
+
import { I } from '@config/IOCIdentifier';
|
|
8
9
|
import { useI18nInterface } from '../hook/useI18nInterface';
|
|
9
10
|
import { useIOC } from '../hook/useIOC';
|
|
10
11
|
import type { PageI18nInterface } from '@config/i18n';
|
|
11
12
|
|
|
12
13
|
export function LogoutButton() {
|
|
13
|
-
const
|
|
14
|
+
const dialogHandler = useIOC(I.DialogHandler);
|
|
15
|
+
const userService = useIOC(I.UserServiceInterface);
|
|
16
|
+
const routerService = useIOC(I.RouterServiceInterface);
|
|
17
|
+
|
|
14
18
|
const tt = useI18nInterface({
|
|
15
19
|
title: AUTH_LOGOUT_DIALOG_TITLE,
|
|
16
20
|
content: AUTH_LOGOUT_DIALOG_CONTENT
|
|
17
21
|
} as PageI18nInterface);
|
|
18
22
|
|
|
19
23
|
const onClick = useCallback(() => {
|
|
20
|
-
|
|
24
|
+
dialogHandler.confirm({
|
|
21
25
|
title: tt.title,
|
|
22
26
|
content: tt.content,
|
|
23
|
-
onOk: () => {
|
|
24
|
-
|
|
27
|
+
onOk: async () => {
|
|
28
|
+
await userService.logout();
|
|
29
|
+
routerService.gotoLogin();
|
|
25
30
|
}
|
|
26
31
|
});
|
|
27
|
-
}, [tt,
|
|
32
|
+
}, [tt, dialogHandler, userService, routerService]);
|
|
28
33
|
|
|
29
34
|
return (
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
<Tooltip data-testid="LogoutIcon" title={tt.title} placement="right">
|
|
36
|
+
<span
|
|
37
|
+
className="text-text hover:text-red-500 cursor-pointer text-lg transition-colors"
|
|
38
|
+
onClick={onClick}
|
|
39
|
+
>
|
|
40
|
+
<LogoutOutlined />
|
|
41
|
+
</span>
|
|
42
|
+
</Tooltip>
|
|
33
43
|
);
|
|
34
44
|
}
|