@qlover/create-app 0.7.13 → 0.7.14
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 +66 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/next-app/config/IOCIdentifier.ts +14 -1
- package/dist/templates/next-app/config/Identifier/index.ts +1 -0
- package/dist/templates/next-app/config/Identifier/page.admin.ts +48 -0
- package/dist/templates/next-app/config/i18n/admin18n.ts +33 -0
- package/dist/templates/next-app/config/i18n/index.ts +3 -1
- package/dist/templates/next-app/migrations/schema/UserSchema.ts +2 -2
- package/dist/templates/next-app/next.config.ts +1 -1
- package/dist/templates/next-app/package.json +3 -1
- package/dist/templates/next-app/public/locales/en.json +8 -1
- package/dist/templates/next-app/public/locales/zh.json +8 -1
- package/dist/templates/next-app/src/app/[locale]/admin/layout.tsx +1 -1
- package/dist/templates/next-app/src/app/[locale]/admin/page.tsx +14 -16
- package/dist/templates/next-app/src/app/[locale]/admin/users/page.tsx +10 -3
- package/dist/templates/next-app/src/app/[locale]/layout.tsx +1 -1
- package/dist/templates/next-app/src/app/[locale]/login/page.tsx +1 -1
- package/dist/templates/next-app/src/app/[locale]/page.tsx +2 -3
- package/dist/templates/next-app/src/app/[locale]/register/page.tsx +1 -1
- package/dist/templates/next-app/src/app/api/ai/completions/route.ts +32 -0
- package/dist/templates/next-app/src/base/cases/AppConfig.ts +3 -0
- package/dist/templates/next-app/src/base/cases/ChatAction.ts +21 -0
- package/dist/templates/next-app/src/base/cases/FocusBarAction.ts +36 -0
- package/dist/templates/next-app/src/base/port/AdminPageInterface.ts +1 -3
- package/dist/templates/next-app/src/base/services/AdminUserService.ts +1 -1
- package/dist/templates/next-app/src/base/services/adminApi/AdminUserApi.ts +1 -1
- package/dist/templates/next-app/src/base/services/appApi/AppApiPlugin.ts +23 -1
- package/dist/templates/next-app/src/base/services/appApi/AppUserApiBootstrap.ts +2 -2
- package/dist/templates/next-app/src/base/types/PageProps.ts +1 -1
- package/dist/templates/next-app/src/core/bootstraps/BootstrapClient.ts +1 -0
- package/dist/templates/next-app/src/core/bootstraps/BootstrapServer.ts +1 -0
- package/dist/templates/next-app/src/core/globals.ts +2 -0
- package/dist/templates/next-app/src/core/serverIoc/ServerIOCRegister.ts +4 -1
- package/dist/templates/next-app/src/{base/cases → server}/PageParams.ts +1 -1
- package/dist/templates/next-app/src/server/port/DBBridgeInterface.ts +31 -0
- package/dist/templates/next-app/src/server/port/DBTableInterface.ts +1 -1
- package/dist/templates/next-app/src/server/repositorys/UserRepository.ts +6 -4
- package/dist/templates/next-app/src/server/services/AIService.ts +43 -0
- package/dist/templates/next-app/src/server/services/ApiUserService.ts +1 -1
- package/dist/templates/next-app/src/server/{SupabaseBridge.ts → sqlBridges/SupabaseBridge.ts} +16 -11
- package/dist/templates/next-app/src/server/validators/LoginValidator.ts +4 -4
- package/dist/templates/next-app/src/server/validators/PaginationValidator.ts +1 -1
- package/dist/templates/next-app/src/uikit/components/AdminLayout.tsx +32 -25
- package/dist/templates/next-app/src/uikit/components/BaseHeader.tsx +12 -26
- package/dist/templates/next-app/src/uikit/components/BaseLayout.tsx +37 -5
- package/dist/templates/next-app/src/uikit/components/ChatRoot.tsx +17 -0
- package/dist/templates/next-app/src/uikit/components/ClientSeo.tsx +36 -0
- package/dist/templates/next-app/src/uikit/components/LanguageSwitcher.tsx +5 -6
- package/dist/templates/next-app/src/uikit/components/LogoutButton.tsx +2 -0
- package/dist/templates/next-app/src/uikit/components/With.tsx +17 -0
- package/dist/templates/next-app/src/uikit/components/chat/ChatActionInterface.ts +30 -0
- package/dist/templates/next-app/src/uikit/components/chat/ChatFocusBar.tsx +65 -0
- package/dist/templates/next-app/src/uikit/components/chat/ChatMessages.tsx +59 -0
- package/dist/templates/next-app/src/uikit/components/chat/ChatWrap.tsx +28 -0
- package/dist/templates/next-app/src/uikit/components/chat/FocusBarActionInterface.ts +19 -0
- package/package.json +1 -1
- package/dist/templates/next-app/src/base/port/DBBridgeInterface.ts +0 -21
- package/dist/templates/next-app/src/base/port/DBMigrationInterface.ts +0 -92
- package/dist/templates/next-app/src/base/port/MigrationApiInterface.ts +0 -3
- package/dist/templates/next-app/src/base/port/ServerApiResponseInterface.ts +0 -6
- package/dist/templates/next-app/src/base/services/migrations/MigrationsApi.ts +0 -43
- package/dist/templates/next-app/config/i18n/{HomeI18n .ts → HomeI18n.ts} +0 -0
- package/dist/templates/next-app/{build → make}/generateLocales.ts +2 -2
- /package/dist/templates/next-app/src/{base → server}/port/PaginationInterface.ts +0 -0
- /package/dist/templates/next-app/src/{base → server}/port/ParamsHandlerInterface.ts +0 -0
|
@@ -6,10 +6,13 @@ import {
|
|
|
6
6
|
} from '@qlover/fe-corekit';
|
|
7
7
|
import type { AppApiErrorInterface } from '@/base/port/AppApiInterface';
|
|
8
8
|
import type { AppApiConfig } from './AppApiRequester';
|
|
9
|
+
import type { LoggerInterface } from '@qlover/logger';
|
|
9
10
|
|
|
10
11
|
export class AppApiPlugin implements ExecutorPlugin {
|
|
11
12
|
readonly pluginName = 'AppApiPlugin';
|
|
12
13
|
|
|
14
|
+
constructor(protected logger: LoggerInterface) {}
|
|
15
|
+
|
|
13
16
|
isAppApiErrorInterface(value: unknown): value is AppApiErrorInterface {
|
|
14
17
|
return (
|
|
15
18
|
typeof value === 'object' &&
|
|
@@ -21,8 +24,16 @@ export class AppApiPlugin implements ExecutorPlugin {
|
|
|
21
24
|
);
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
onSuccess(context: ExecutorContext<
|
|
27
|
+
onSuccess(context: ExecutorContext<AppApiConfig>): void | Promise<void> {
|
|
25
28
|
const response = context.returnValue;
|
|
29
|
+
const { parameters } = context;
|
|
30
|
+
|
|
31
|
+
this.logger.info(
|
|
32
|
+
`%c[AppApi ${parameters.method} ${parameters.url}]%c - ${new Date().toLocaleString()}`,
|
|
33
|
+
'color: #0f0;',
|
|
34
|
+
'color: inherit;',
|
|
35
|
+
response
|
|
36
|
+
);
|
|
26
37
|
|
|
27
38
|
if (this.isAppApiErrorInterface(response)) {
|
|
28
39
|
throw new Error(response.message || response.id);
|
|
@@ -34,6 +45,8 @@ export class AppApiPlugin implements ExecutorPlugin {
|
|
|
34
45
|
): Promise<ExecutorError | void> {
|
|
35
46
|
const { error, parameters } = context;
|
|
36
47
|
|
|
48
|
+
this.loggerError(parameters, error);
|
|
49
|
+
|
|
37
50
|
if (error instanceof RequestError && parameters.responseType === 'json') {
|
|
38
51
|
// @ts-expect-error response is not defined in Error
|
|
39
52
|
let response = error?.response;
|
|
@@ -60,4 +73,13 @@ export class AppApiPlugin implements ExecutorPlugin {
|
|
|
60
73
|
return {};
|
|
61
74
|
}
|
|
62
75
|
}
|
|
76
|
+
|
|
77
|
+
protected loggerError(config: AppApiConfig, error: unknown): void {
|
|
78
|
+
this.logger.error(
|
|
79
|
+
`%c[AppApi ${config.method} ${config.url}]%c - ${new Date().toLocaleString()}`,
|
|
80
|
+
'color: #f00;',
|
|
81
|
+
'color: inherit;',
|
|
82
|
+
error
|
|
83
|
+
);
|
|
84
|
+
}
|
|
63
85
|
}
|
|
@@ -3,6 +3,7 @@ import { FetchURLPlugin } from '@qlover/fe-corekit';
|
|
|
3
3
|
import { DialogErrorPlugin } from '@/base/cases/DialogErrorPlugin';
|
|
4
4
|
import { RequestEncryptPlugin } from '@/base/cases/RequestEncryptPlugin';
|
|
5
5
|
import { StringEncryptor } from '@/base/cases/StringEncryptor';
|
|
6
|
+
import { I } from '@config/IOCIdentifier';
|
|
6
7
|
import { AppApiPlugin } from './AppApiPlugin';
|
|
7
8
|
import { AppApiRequester } from './AppApiRequester';
|
|
8
9
|
import type { AppApiConfig } from './AppApiRequester';
|
|
@@ -27,9 +28,8 @@ export class AppUserApiBootstrap implements BootstrapExecutorPlugin {
|
|
|
27
28
|
requestDataSerializer: this.requestDataSerializer.bind(this)
|
|
28
29
|
})
|
|
29
30
|
);
|
|
30
|
-
appUserApi.usePlugin(new AppApiPlugin());
|
|
31
|
+
appUserApi.usePlugin(new AppApiPlugin(ioc.get(I.Logger)));
|
|
31
32
|
appUserApi.usePlugin(ioc.get(DialogErrorPlugin));
|
|
32
|
-
console.log('jj AppUserApiBootstrap success');
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
protected requestDataSerializer(
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type IOCRegisterInterface
|
|
8
8
|
} from '@qlover/corekit-bridge';
|
|
9
9
|
import type { IocRegisterOptions } from '@/base/port/IOCInterface';
|
|
10
|
+
import { SupabaseBridge } from '@/server/sqlBridges/SupabaseBridge';
|
|
10
11
|
import { IOCIdentifier as I } from '@config/IOCIdentifier';
|
|
11
12
|
|
|
12
13
|
export class ServerIOCRegister
|
|
@@ -45,7 +46,9 @@ export class ServerIOCRegister
|
|
|
45
46
|
);
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
protected registerImplement(
|
|
49
|
+
protected registerImplement(ioc: IOCContainerInterface): void {
|
|
50
|
+
ioc.bind(I.DBBridgeInterface, ioc.get(SupabaseBridge));
|
|
51
|
+
}
|
|
49
52
|
|
|
50
53
|
protected registerCommon(_ioc: IOCContainerInterface): void {}
|
|
51
54
|
|
|
@@ -2,7 +2,7 @@ import { notFound } from 'next/navigation';
|
|
|
2
2
|
import { getMessages, getTranslations } from 'next-intl/server';
|
|
3
3
|
import { i18nConfig } from '@config/i18n';
|
|
4
4
|
import type { LocaleType, PageI18nInterface } from '@config/i18n';
|
|
5
|
-
import type { ParamsHandlerInterface as ParamsHandlerInterface } from '
|
|
5
|
+
import type { ParamsHandlerInterface as ParamsHandlerInterface } from './port/ParamsHandlerInterface';
|
|
6
6
|
|
|
7
7
|
export interface PageWithParams {
|
|
8
8
|
params?: Promise<PageParamsType>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type WhereOperation = '=' | '!=' | '>' | '<' | '>=' | '<=';
|
|
2
|
+
export type Where = [string, WhereOperation, string | number];
|
|
3
|
+
|
|
4
|
+
export interface BridgeEvent {
|
|
5
|
+
table: string;
|
|
6
|
+
fields?: string | string[];
|
|
7
|
+
where?: Where[];
|
|
8
|
+
data?: unknown;
|
|
9
|
+
page?: number;
|
|
10
|
+
pageSize?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PaginationInfo {
|
|
14
|
+
total: number;
|
|
15
|
+
page: number;
|
|
16
|
+
pageSize: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DBBridgeResponse<T> {
|
|
20
|
+
error?: unknown;
|
|
21
|
+
data: T;
|
|
22
|
+
pagination?: PaginationInfo;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DBBridgeInterface {
|
|
26
|
+
add(event: BridgeEvent): Promise<DBBridgeResponse<unknown>>;
|
|
27
|
+
update(event: BridgeEvent): Promise<DBBridgeResponse<unknown>>;
|
|
28
|
+
delete(event: BridgeEvent): Promise<DBBridgeResponse<unknown>>;
|
|
29
|
+
get(event: BridgeEvent): Promise<DBBridgeResponse<unknown>>;
|
|
30
|
+
pagination(event: BridgeEvent): Promise<DBBridgeResponse<unknown[]>>;
|
|
31
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { inject, injectable } from 'inversify';
|
|
2
2
|
import { isEmpty, last } from 'lodash';
|
|
3
|
-
import type { DBBridgeInterface } from '@/
|
|
4
|
-
import type { PaginationInterface } from '@/
|
|
3
|
+
import type { DBBridgeInterface } from '@/server/port/DBBridgeInterface';
|
|
4
|
+
import type { PaginationInterface } from '@/server/port/PaginationInterface';
|
|
5
5
|
import type { UserSchema } from '@migrations/schema/UserSchema';
|
|
6
|
-
import {
|
|
6
|
+
import { I } from '@config/IOCIdentifier';
|
|
7
7
|
import type { UserRepositoryInterface } from '../port/UserRepositoryInterface';
|
|
8
8
|
|
|
9
9
|
@injectable()
|
|
@@ -21,7 +21,9 @@ export class UserRepository implements UserRepositoryInterface {
|
|
|
21
21
|
'updated_at'
|
|
22
22
|
];
|
|
23
23
|
|
|
24
|
-
constructor(
|
|
24
|
+
constructor(
|
|
25
|
+
@inject(I.DBBridgeInterface) protected dbBridge: DBBridgeInterface
|
|
26
|
+
) {}
|
|
25
27
|
|
|
26
28
|
getAll(): Promise<unknown> {
|
|
27
29
|
return this.dbBridge.get({ table: this.name });
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { inject, injectable } from 'inversify';
|
|
2
|
+
import OpenAI from 'openai';
|
|
3
|
+
import type { AppConfig } from '@/base/cases/AppConfig';
|
|
4
|
+
import { I } from '@config/IOCIdentifier';
|
|
5
|
+
import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions';
|
|
6
|
+
|
|
7
|
+
@injectable()
|
|
8
|
+
export class AIService {
|
|
9
|
+
protected apiKey: string;
|
|
10
|
+
protected baseUrl: string;
|
|
11
|
+
protected client: OpenAI;
|
|
12
|
+
|
|
13
|
+
constructor(@inject(I.AppConfig) appConfig: AppConfig) {
|
|
14
|
+
this.apiKey = appConfig.openaiApiKey;
|
|
15
|
+
this.baseUrl = appConfig.openaiBaseUrl;
|
|
16
|
+
|
|
17
|
+
console.log(this.apiKey, this.baseUrl);
|
|
18
|
+
this.client = new OpenAI({
|
|
19
|
+
apiKey: this.apiKey,
|
|
20
|
+
baseURL: this.baseUrl
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async completions(messages: ChatCompletionMessageParam[]): Promise<unknown> {
|
|
25
|
+
const url = `${this.baseUrl}/chat/completions`;
|
|
26
|
+
|
|
27
|
+
const response = await fetch(url, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
messages: messages,
|
|
31
|
+
model: 'claude-sonnet-4-20250514'
|
|
32
|
+
}),
|
|
33
|
+
headers: {
|
|
34
|
+
Authorization: `token ${this.apiKey}`,
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
Accept: 'application/json'
|
|
37
|
+
},
|
|
38
|
+
mode: 'cors'
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return await response.json();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { inject, injectable } from 'inversify';
|
|
2
|
-
import type { PaginationInterface } from '@/
|
|
2
|
+
import type { PaginationInterface } from '@/server/port/PaginationInterface';
|
|
3
3
|
import type { UserSchema } from '@migrations/schema/UserSchema';
|
|
4
4
|
import { UserRepository } from '../repositorys/UserRepository';
|
|
5
5
|
import { PaginationValidator } from '../validators/PaginationValidator';
|
package/dist/templates/next-app/src/server/{SupabaseBridge.ts → sqlBridges/SupabaseBridge.ts}
RENAMED
|
@@ -9,8 +9,9 @@ import type { AppConfig } from '@/base/cases/AppConfig';
|
|
|
9
9
|
import type {
|
|
10
10
|
BridgeEvent,
|
|
11
11
|
DBBridgeInterface,
|
|
12
|
+
DBBridgeResponse,
|
|
12
13
|
Where
|
|
13
|
-
} from '@/
|
|
14
|
+
} from '@/server/port/DBBridgeInterface';
|
|
14
15
|
import { I } from '@config/IOCIdentifier';
|
|
15
16
|
import type { LoggerInterface } from '@qlover/logger';
|
|
16
17
|
import type { PostgrestFilterBuilder } from '@supabase/postgrest-js';
|
|
@@ -24,6 +25,9 @@ const whereHandlerMaps = {
|
|
|
24
25
|
'<=': 'lte'
|
|
25
26
|
};
|
|
26
27
|
|
|
28
|
+
export type SupabaseBridgeResponse<T> = DBBridgeResponse<T> &
|
|
29
|
+
PostgrestResponse<T>;
|
|
30
|
+
|
|
27
31
|
@injectable()
|
|
28
32
|
export class SupabaseBridge implements DBBridgeInterface {
|
|
29
33
|
protected supabase: SupabaseClient;
|
|
@@ -42,19 +46,20 @@ export class SupabaseBridge implements DBBridgeInterface {
|
|
|
42
46
|
return this.supabase;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
async execSql(sql: string): Promise<
|
|
49
|
+
async execSql(sql: string): Promise<SupabaseBridgeResponse<unknown>> {
|
|
46
50
|
const res = await this.supabase.rpc('exec_sql', { sql });
|
|
47
51
|
return this.catch(res);
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
protected async catch(
|
|
51
55
|
result: PostgrestSingleResponse<unknown>
|
|
52
|
-
): Promise<
|
|
56
|
+
): Promise<SupabaseBridgeResponse<unknown>> {
|
|
53
57
|
if (result.error) {
|
|
54
58
|
this.logger.info(result);
|
|
55
59
|
throw new Error(result.error.message);
|
|
56
60
|
}
|
|
57
|
-
|
|
61
|
+
|
|
62
|
+
return result as SupabaseBridgeResponse<unknown>;
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
protected handleWhere(
|
|
@@ -79,7 +84,7 @@ export class SupabaseBridge implements DBBridgeInterface {
|
|
|
79
84
|
}
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
async add(event: BridgeEvent): Promise<
|
|
87
|
+
async add(event: BridgeEvent): Promise<DBBridgeResponse<unknown>> {
|
|
83
88
|
const { table, data } = event;
|
|
84
89
|
if (!data) {
|
|
85
90
|
throw new Error('Data is required for add operation');
|
|
@@ -91,7 +96,7 @@ export class SupabaseBridge implements DBBridgeInterface {
|
|
|
91
96
|
return this.catch(res);
|
|
92
97
|
}
|
|
93
98
|
|
|
94
|
-
async update(event: BridgeEvent): Promise<
|
|
99
|
+
async update(event: BridgeEvent): Promise<DBBridgeResponse<unknown>> {
|
|
95
100
|
const { table, data, where } = event;
|
|
96
101
|
if (!data) {
|
|
97
102
|
throw new Error('Data is required for update operation');
|
|
@@ -104,7 +109,7 @@ export class SupabaseBridge implements DBBridgeInterface {
|
|
|
104
109
|
return this.catch(await handler);
|
|
105
110
|
}
|
|
106
111
|
|
|
107
|
-
async delete(event: BridgeEvent): Promise<
|
|
112
|
+
async delete(event: BridgeEvent): Promise<DBBridgeResponse<unknown>> {
|
|
108
113
|
const { table, where } = event;
|
|
109
114
|
const handler = this.supabase.from(table).delete();
|
|
110
115
|
|
|
@@ -113,7 +118,7 @@ export class SupabaseBridge implements DBBridgeInterface {
|
|
|
113
118
|
return this.catch(await handler);
|
|
114
119
|
}
|
|
115
120
|
|
|
116
|
-
async get(event: BridgeEvent): Promise<
|
|
121
|
+
async get(event: BridgeEvent): Promise<SupabaseBridgeResponse<unknown>> {
|
|
117
122
|
const { table, fields = '*', where } = event;
|
|
118
123
|
const selectFields = Array.isArray(fields) ? fields.join(',') : fields;
|
|
119
124
|
const handler = this.supabase.from(table).select(selectFields);
|
|
@@ -123,7 +128,7 @@ export class SupabaseBridge implements DBBridgeInterface {
|
|
|
123
128
|
return this.catch(await handler);
|
|
124
129
|
}
|
|
125
130
|
|
|
126
|
-
async pagination(event: BridgeEvent): Promise<
|
|
131
|
+
async pagination(event: BridgeEvent): Promise<DBBridgeResponse<unknown[]>> {
|
|
127
132
|
const { table, fields = '*', where, page = 1, pageSize = 10 } = event;
|
|
128
133
|
const selectFields = Array.isArray(fields) ? fields.join(',') : fields;
|
|
129
134
|
|
|
@@ -145,7 +150,7 @@ export class SupabaseBridge implements DBBridgeInterface {
|
|
|
145
150
|
const result = await this.catch(await handler);
|
|
146
151
|
|
|
147
152
|
if (result.error) {
|
|
148
|
-
return result as
|
|
153
|
+
return result as DBBridgeResponse<unknown[]>;
|
|
149
154
|
}
|
|
150
155
|
|
|
151
156
|
return {
|
|
@@ -154,6 +159,6 @@ export class SupabaseBridge implements DBBridgeInterface {
|
|
|
154
159
|
count: countResult.count,
|
|
155
160
|
status: result.status,
|
|
156
161
|
statusText: result.statusText
|
|
157
|
-
}
|
|
162
|
+
} as DBBridgeResponse<unknown[]>;
|
|
158
163
|
}
|
|
159
164
|
}
|
|
@@ -17,13 +17,13 @@ export interface LoginValidatorData {
|
|
|
17
17
|
password: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const emailSchema = z.email({
|
|
20
|
+
const emailSchema = z.string().email({ message: V_EMAIL_INVALID });
|
|
21
21
|
|
|
22
22
|
const passwordSchema = z
|
|
23
23
|
.string()
|
|
24
|
-
.min(6, {
|
|
25
|
-
.max(50, {
|
|
26
|
-
.regex(/^\S+$/, {
|
|
24
|
+
.min(6, { message: V_PASSWORD_MIN_LENGTH })
|
|
25
|
+
.max(50, { message: V_PASSWORD_MAX_LENGTH })
|
|
26
|
+
.regex(/^\S+$/, { message: V_PASSWORD_SPECIAL_CHARS });
|
|
27
27
|
|
|
28
28
|
interface ExtendedExecutorError extends ExecutorError {
|
|
29
29
|
issues?: ValidationFaildResult[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import type { PaginationInterface } from '@/
|
|
2
|
+
import type { PaginationInterface } from '@/server/port/PaginationInterface';
|
|
3
3
|
import { API_PAGE_INVALID } from '@config/Identifier';
|
|
4
4
|
import {
|
|
5
5
|
type ValidationFaildResult,
|
|
@@ -3,18 +3,20 @@
|
|
|
3
3
|
import {
|
|
4
4
|
DashboardOutlined,
|
|
5
5
|
UserOutlined,
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
VerticalAlignBottomOutlined,
|
|
7
|
+
VerticalAlignTopOutlined
|
|
8
8
|
} from '@ant-design/icons';
|
|
9
9
|
import { Layout, Menu } from 'antd';
|
|
10
10
|
import { clsx } from 'clsx';
|
|
11
|
-
import React, {
|
|
11
|
+
import React, { useMemo, type HTMLAttributes } from 'react';
|
|
12
12
|
import { AdminPageManager } from '@/base/cases/AdminPageManager';
|
|
13
13
|
import { BaseHeader } from './BaseHeader';
|
|
14
|
+
import { LanguageSwitcher } from './LanguageSwitcher';
|
|
14
15
|
import { LocaleLink } from './LocaleLink';
|
|
16
|
+
import { LogoutButton } from './LogoutButton';
|
|
17
|
+
import { ThemeSwitcher } from './ThemeSwitcher';
|
|
15
18
|
import { useIOC } from '../hook/useIOC';
|
|
16
19
|
import { useStore } from '../hook/useStore';
|
|
17
|
-
import type { RenderLeftFunction } from './BaseHeader';
|
|
18
20
|
import type { ItemType } from 'antd/es/menu/interface';
|
|
19
21
|
|
|
20
22
|
const { Sider } = Layout;
|
|
@@ -33,7 +35,6 @@ export function AdminLayout(props: AdminLayoutProps) {
|
|
|
33
35
|
const { children, className, mainClassName, ...rest } = props;
|
|
34
36
|
|
|
35
37
|
const page = useIOC(AdminPageManager);
|
|
36
|
-
|
|
37
38
|
const collapsedSidebar = useStore(page, page.selectors.collapsedSidebar);
|
|
38
39
|
const navItems = useStore(page, page.selectors.navItems);
|
|
39
40
|
|
|
@@ -60,40 +61,46 @@ export function AdminLayout(props: AdminLayoutProps) {
|
|
|
60
61
|
});
|
|
61
62
|
}, [navItems]);
|
|
62
63
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
{collapsedSidebar ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
|
71
|
-
</span>
|
|
72
|
-
|
|
73
|
-
{defaultElement}
|
|
74
|
-
</div>
|
|
75
|
-
),
|
|
76
|
-
[collapsedSidebar, page]
|
|
77
|
-
);
|
|
64
|
+
const rightActions = useMemo(() => {
|
|
65
|
+
return [
|
|
66
|
+
<LanguageSwitcher key="language-switcher" />,
|
|
67
|
+
<ThemeSwitcher key="theme-switcher" />,
|
|
68
|
+
<LogoutButton key="logout-button" />
|
|
69
|
+
];
|
|
70
|
+
}, []);
|
|
78
71
|
|
|
79
72
|
return (
|
|
80
73
|
<Layout data-testid="AdminLayout" className={className} {...rest}>
|
|
81
|
-
<div className="overflow-auto h-screen sticky top-0 bottom-0 scrollbar-thin scrollbar-gutter-stable">
|
|
74
|
+
<div className="overflow-y-auto overflow-x-hidden h-screen sticky top-0 bottom-0 scrollbar-thin scrollbar-gutter-stable">
|
|
82
75
|
<Sider
|
|
83
|
-
className="h-full"
|
|
76
|
+
className="h-full relative"
|
|
84
77
|
onCollapse={() => page.toggleSidebar()}
|
|
85
78
|
collapsed={collapsedSidebar}
|
|
79
|
+
collapsedWidth={46}
|
|
86
80
|
>
|
|
87
|
-
<div className="demo-logo-vertical" />
|
|
88
81
|
<Menu mode="inline" items={sidebarItems} />
|
|
82
|
+
|
|
83
|
+
<div
|
|
84
|
+
data-testid="ToggleSidebarButton"
|
|
85
|
+
className="absolute w-2 right-0 top-0 bottom-0 bg-secondary cursor-pointer hover:bg-elevated flex items-center justify-center"
|
|
86
|
+
onClick={() => page.toggleSidebar()}
|
|
87
|
+
>
|
|
88
|
+
<span className="text-text scale-75 rotate-x-90">
|
|
89
|
+
{collapsedSidebar ? (
|
|
90
|
+
<VerticalAlignTopOutlined />
|
|
91
|
+
) : (
|
|
92
|
+
<VerticalAlignBottomOutlined />
|
|
93
|
+
)}
|
|
94
|
+
</span>
|
|
95
|
+
</div>
|
|
89
96
|
</Sider>
|
|
90
97
|
</div>
|
|
91
98
|
|
|
92
99
|
<Layout>
|
|
93
100
|
<BaseHeader
|
|
94
101
|
href="/admin"
|
|
95
|
-
|
|
96
|
-
|
|
102
|
+
className="max-w-full pl-0"
|
|
103
|
+
rightActions={rightActions}
|
|
97
104
|
/>
|
|
98
105
|
<main
|
|
99
106
|
className={clsx('p-2 bg-primary text-text flex-1', mainClassName)}
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { clsx } from 'clsx';
|
|
4
4
|
import { useLocale, useTranslations } from 'next-intl';
|
|
5
5
|
import { useMemo } from 'react';
|
|
6
6
|
import { useIOC } from '@/uikit/hook/useIOC';
|
|
7
7
|
import { PAGE_HEAD_ADMIN_TITLE } from '@config/Identifier';
|
|
8
8
|
import { IOCIdentifier } from '@config/IOCIdentifier';
|
|
9
|
-
import { LanguageSwitcher } from './LanguageSwitcher';
|
|
10
9
|
import { LocaleLink } from './LocaleLink';
|
|
11
|
-
import { LogoutButton } from './LogoutButton';
|
|
12
|
-
import { ThemeSwitcher } from './ThemeSwitcher';
|
|
13
10
|
|
|
14
11
|
export type RenderLeftFunction = (props: {
|
|
15
12
|
locale: string;
|
|
@@ -18,13 +15,12 @@ export type RenderLeftFunction = (props: {
|
|
|
18
15
|
|
|
19
16
|
export function BaseHeader(props: {
|
|
20
17
|
href?: string;
|
|
21
|
-
showLogoutButton?: boolean;
|
|
22
|
-
showAdminButton?: boolean;
|
|
23
18
|
renderLeft?: React.ReactNode | RenderLeftFunction;
|
|
19
|
+
rightActions?: React.ReactNode;
|
|
20
|
+
className?: string;
|
|
24
21
|
}) {
|
|
25
|
-
const { href = '/',
|
|
22
|
+
const { href = '/', className, renderLeft, rightActions } = props;
|
|
26
23
|
const appConfig = useIOC(IOCIdentifier.AppConfig);
|
|
27
|
-
const i18nService = useIOC(IOCIdentifier.I18nServiceInterface);
|
|
28
24
|
const locale = useLocale();
|
|
29
25
|
const t = useTranslations();
|
|
30
26
|
|
|
@@ -44,7 +40,7 @@ export function BaseHeader(props: {
|
|
|
44
40
|
>
|
|
45
41
|
<span
|
|
46
42
|
data-testid="base-header-app-name"
|
|
47
|
-
className="
|
|
43
|
+
className="text-lg font-semibold text-text"
|
|
48
44
|
>
|
|
49
45
|
{tt.title}
|
|
50
46
|
</span>
|
|
@@ -69,24 +65,14 @@ export function BaseHeader(props: {
|
|
|
69
65
|
data-testid="BaseHeader"
|
|
70
66
|
className="h-14 bg-secondary border-b border-c-border sticky top-0 z-50"
|
|
71
67
|
>
|
|
72
|
-
<div
|
|
68
|
+
<div
|
|
69
|
+
className={clsx(
|
|
70
|
+
'flex items-center justify-between h-full px-4 mx-auto max-w-7xl',
|
|
71
|
+
className
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
73
74
|
<div className="flex items-center">{RenderLeft}</div>
|
|
74
|
-
<div className="flex items-center gap-2 md:gap-4">
|
|
75
|
-
{showAdminButton && (
|
|
76
|
-
<LocaleLink
|
|
77
|
-
href="/admin"
|
|
78
|
-
title={tt.admin}
|
|
79
|
-
locale={locale}
|
|
80
|
-
className="text-text hover:text-text-hover cursor-pointer text-lg transition-colors"
|
|
81
|
-
>
|
|
82
|
-
<TeamOutlined className="text-lg text-text" />
|
|
83
|
-
</LocaleLink>
|
|
84
|
-
)}
|
|
85
|
-
|
|
86
|
-
<LanguageSwitcher i18nService={i18nService} />
|
|
87
|
-
<ThemeSwitcher />
|
|
88
|
-
{showLogoutButton && <LogoutButton />}
|
|
89
|
-
</div>
|
|
75
|
+
<div className="flex items-center gap-2 md:gap-4">{rightActions}</div>
|
|
90
76
|
</div>
|
|
91
77
|
</header>
|
|
92
78
|
);
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
import { TeamOutlined } from '@ant-design/icons';
|
|
2
|
+
import { useLocale, useTranslations } from 'next-intl';
|
|
3
|
+
import { useMemo, type HTMLAttributes } from 'react';
|
|
4
|
+
import { PAGE_HEAD_ADMIN_TITLE } from '@config/Identifier';
|
|
1
5
|
import { BaseHeader } from './BaseHeader';
|
|
2
|
-
import
|
|
6
|
+
import { LanguageSwitcher } from './LanguageSwitcher';
|
|
7
|
+
import { LocaleLink } from './LocaleLink';
|
|
8
|
+
import { LogoutButton } from './LogoutButton';
|
|
9
|
+
import { ThemeSwitcher } from './ThemeSwitcher';
|
|
3
10
|
|
|
4
11
|
export interface BaseLayoutProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
12
|
showLogoutButton?: boolean;
|
|
@@ -14,16 +21,41 @@ export function BaseLayout({
|
|
|
14
21
|
mainProps,
|
|
15
22
|
...props
|
|
16
23
|
}: BaseLayoutProps) {
|
|
24
|
+
const locale = useLocale();
|
|
25
|
+
const t = useTranslations();
|
|
26
|
+
|
|
27
|
+
const tt = {
|
|
28
|
+
admin: t(PAGE_HEAD_ADMIN_TITLE)
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const actions = useMemo(
|
|
32
|
+
() =>
|
|
33
|
+
[
|
|
34
|
+
showAdminButton && (
|
|
35
|
+
<LocaleLink
|
|
36
|
+
key="admin-button"
|
|
37
|
+
href="/admin"
|
|
38
|
+
title={tt.admin}
|
|
39
|
+
locale={locale}
|
|
40
|
+
className="text-text hover:text-text-hover cursor-pointer text-lg transition-colors"
|
|
41
|
+
>
|
|
42
|
+
<TeamOutlined className="text-lg text-text" />
|
|
43
|
+
</LocaleLink>
|
|
44
|
+
),
|
|
45
|
+
<LanguageSwitcher key="language-switcher" />,
|
|
46
|
+
<ThemeSwitcher key="theme-switcher" />,
|
|
47
|
+
showLogoutButton && <LogoutButton key="logout-button" />
|
|
48
|
+
].filter(Boolean),
|
|
49
|
+
[showAdminButton, tt.admin, locale, showLogoutButton]
|
|
50
|
+
);
|
|
51
|
+
|
|
17
52
|
return (
|
|
18
53
|
<div
|
|
19
54
|
data-testid="BaseLayout"
|
|
20
55
|
className="flex flex-col min-h-screen"
|
|
21
56
|
{...props}
|
|
22
57
|
>
|
|
23
|
-
<BaseHeader
|
|
24
|
-
showLogoutButton={showLogoutButton}
|
|
25
|
-
showAdminButton={showAdminButton}
|
|
26
|
-
/>
|
|
58
|
+
<BaseHeader rightActions={actions} />
|
|
27
59
|
<main className="flex flex-1 flex-col bg-primary" {...mainProps}>
|
|
28
60
|
{children}
|
|
29
61
|
</main>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ChatAction } from '@/base/cases/ChatAction';
|
|
4
|
+
import { FocusBarAction } from '@/base/cases/FocusBarAction';
|
|
5
|
+
import { useIOC } from '../hook/useIOC';
|
|
6
|
+
import { ChatWrap } from './chat/ChatWrap';
|
|
7
|
+
|
|
8
|
+
export function ChatRoot() {
|
|
9
|
+
const chatAction = useIOC(ChatAction);
|
|
10
|
+
const focusBarAction = useIOC(FocusBarAction);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div data-testid="ChatRoot" className="fixed bottom-0 right-0 ">
|
|
14
|
+
<ChatWrap chatAction={chatAction} focusBarAction={focusBarAction} />
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PageI18nInterface } from '@config/i18n';
|
|
2
|
+
import { With } from './With';
|
|
3
|
+
|
|
4
|
+
export function ClientSeo(props: {
|
|
5
|
+
i18nInterface: PageI18nInterface;
|
|
6
|
+
children?: React.ReactNode;
|
|
7
|
+
}) {
|
|
8
|
+
const { i18nInterface, children } = props;
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<>
|
|
12
|
+
<title>{i18nInterface.title}</title>
|
|
13
|
+
<meta name="description" content={i18nInterface.description} />
|
|
14
|
+
<meta name="keywords" content={i18nInterface.keywords} />
|
|
15
|
+
<meta name="author" content={i18nInterface.author} />
|
|
16
|
+
<meta name="publishedTime" content={i18nInterface.publishedTime} />
|
|
17
|
+
<meta name="modifiedTime" content={i18nInterface.modifiedTime} />
|
|
18
|
+
|
|
19
|
+
<With it={i18nInterface.canonical}>
|
|
20
|
+
<meta name="canonical" content={i18nInterface.canonical} />
|
|
21
|
+
</With>
|
|
22
|
+
|
|
23
|
+
<With it={i18nInterface.og}>
|
|
24
|
+
{({ title, description, image }) => (
|
|
25
|
+
<>
|
|
26
|
+
<meta name="og:title" content={title} />
|
|
27
|
+
<meta name="og:description" content={description} />
|
|
28
|
+
<meta name="og:image" content={image} />
|
|
29
|
+
</>
|
|
30
|
+
)}
|
|
31
|
+
</With>
|
|
32
|
+
|
|
33
|
+
{children}
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
}
|