create-pardx-scaffold 0.1.6 → 0.1.8
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/package.json +1 -1
- package/template/apps/api/prisma.config.ts +29 -0
- package/template/apps/web/i18n/config.ts +0 -1
- package/template/apps/web/i18n/types.ts +0 -3
- package/template/apps/web/lib/analytics/components/PageTracker.tsx +13 -11
- package/template/apps/web/proxy.ts +1 -41
- package/template/packages/utils/string.util.ts +0 -8
- package/template/scripts/init-project.js +5 -25
package/package.json
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prisma 7.x Configuration File
|
|
3
|
+
*
|
|
4
|
+
* This file is used by Prisma CLI commands (migrate, db push, etc.)
|
|
5
|
+
* For PrismaClient initialization, the URL is passed via the driver adapter
|
|
6
|
+
* in PrismaWriteService and PrismaReadService.
|
|
7
|
+
*
|
|
8
|
+
* 注意: 生成器配置(如 output 路径)需要在 schema.prisma 中配置,
|
|
9
|
+
* 不在此文件中配置。
|
|
10
|
+
*
|
|
11
|
+
* @see https://www.prisma.io/docs/orm/more/upgrade-guides/upgrading-versions/upgrading-to-prisma-7
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as dotenv from 'dotenv';
|
|
15
|
+
import * as dotenvExpand from 'dotenv-expand';
|
|
16
|
+
import { defineConfig, env } from 'prisma/config';
|
|
17
|
+
|
|
18
|
+
// 使用 dotenv-expand 展开环境变量(如 ${BASE_HOST})
|
|
19
|
+
dotenvExpand.expand(dotenv.config());
|
|
20
|
+
|
|
21
|
+
export default defineConfig({
|
|
22
|
+
schema: 'prisma/schema.prisma',
|
|
23
|
+
migrations: {
|
|
24
|
+
path: 'prisma/migrations',
|
|
25
|
+
},
|
|
26
|
+
datasource: {
|
|
27
|
+
url: env('DATABASE_URL'),
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -20,7 +20,6 @@ import type zhCNSettings from '../locales/zh-CN/settings.json';
|
|
|
20
20
|
import type zhCNSubscription from '../locales/zh-CN/subscription.json';
|
|
21
21
|
import type zhCNRecommendation from '../locales/zh-CN/recommendation.json';
|
|
22
22
|
import type zhCNMemory from '../locales/zh-CN/memory.json';
|
|
23
|
-
import type zhCNDailyChallenge from '../locales/zh-CN/daily-challenge.json';
|
|
24
23
|
|
|
25
24
|
/**
|
|
26
25
|
* 所有翻译消息的类型定义
|
|
@@ -39,7 +38,6 @@ export interface AppMessages {
|
|
|
39
38
|
subscription: typeof zhCNSubscription;
|
|
40
39
|
recommendation: typeof zhCNRecommendation;
|
|
41
40
|
memory: typeof zhCNMemory;
|
|
42
|
-
'daily-challenge': typeof zhCNDailyChallenge;
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
/**
|
|
@@ -49,7 +47,6 @@ export interface AppMessages {
|
|
|
49
47
|
declare global {
|
|
50
48
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
51
49
|
namespace IntlMessages {
|
|
52
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
53
50
|
interface Messages extends AppMessages {}
|
|
54
51
|
}
|
|
55
52
|
}
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import { useEffect } from 'react';
|
|
24
24
|
import { usePathname, useSearchParams } from 'next/navigation';
|
|
25
25
|
import { analytics } from '../index';
|
|
26
|
+
import type { EventProperties } from '@repo/contracts/schemas/analytics.schema';
|
|
26
27
|
|
|
27
28
|
interface PageTrackerProps {
|
|
28
29
|
/**
|
|
@@ -56,9 +57,10 @@ export function PageTracker({
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
// Build full path with search params if enabled
|
|
59
|
-
const fullPath =
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
const fullPath =
|
|
61
|
+
trackSearchParams && searchParams.toString()
|
|
62
|
+
? `${pathname}?${searchParams.toString()}`
|
|
63
|
+
: pathname;
|
|
62
64
|
|
|
63
65
|
// Get page name (custom or pathname)
|
|
64
66
|
const pageName = pageNameMapper ? pageNameMapper(pathname) : pathname;
|
|
@@ -67,10 +69,13 @@ export function PageTracker({
|
|
|
67
69
|
analytics.pageView(fullPath, pageName);
|
|
68
70
|
|
|
69
71
|
// Track session start on first page view
|
|
70
|
-
if (
|
|
72
|
+
if (
|
|
73
|
+
typeof window !== 'undefined' &&
|
|
74
|
+
!sessionStorage.getItem('session_started')
|
|
75
|
+
) {
|
|
71
76
|
analytics.track('SESSION_START', {
|
|
72
77
|
path: fullPath,
|
|
73
|
-
} as
|
|
78
|
+
} as unknown as EventProperties);
|
|
74
79
|
sessionStorage.setItem('session_started', 'true');
|
|
75
80
|
}
|
|
76
81
|
}, [pathname, searchParams, trackSearchParams, pageNameMapper, excludePaths]);
|
|
@@ -80,7 +85,7 @@ export function PageTracker({
|
|
|
80
85
|
const handleBeforeUnload = () => {
|
|
81
86
|
analytics.track('SESSION_END', {
|
|
82
87
|
path: pathname,
|
|
83
|
-
} as
|
|
88
|
+
} as unknown as EventProperties);
|
|
84
89
|
};
|
|
85
90
|
|
|
86
91
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
@@ -96,11 +101,11 @@ export function PageTracker({
|
|
|
96
101
|
if (document.hidden) {
|
|
97
102
|
analytics.track('APP_BACKGROUND', {
|
|
98
103
|
path: pathname,
|
|
99
|
-
} as
|
|
104
|
+
} as unknown as EventProperties);
|
|
100
105
|
} else {
|
|
101
106
|
analytics.track('APP_FOREGROUND', {
|
|
102
107
|
path: pathname,
|
|
103
|
-
} as
|
|
108
|
+
} as unknown as EventProperties);
|
|
104
109
|
}
|
|
105
110
|
};
|
|
106
111
|
|
|
@@ -121,9 +126,6 @@ export const defaultPageNameMapper = (pathname: string): string => {
|
|
|
121
126
|
const routes: Record<string, string> = {
|
|
122
127
|
'/': 'Home',
|
|
123
128
|
'/home': 'Home',
|
|
124
|
-
'/daily-challenge': 'Daily Challenge',
|
|
125
|
-
'/daily-challenge/result': 'Challenge Result',
|
|
126
|
-
'/daily-challenge/history': 'Challenge History',
|
|
127
129
|
'/settings': 'Settings',
|
|
128
130
|
'/login': 'Login',
|
|
129
131
|
'/register': 'Register',
|
|
@@ -32,7 +32,7 @@ export default async function middleware(request: NextRequest) {
|
|
|
32
32
|
const pathWithoutLocale = pathname.replace(/^\/(zh-CN|en)/, '') || '/';
|
|
33
33
|
|
|
34
34
|
// Skip daily check-in logic for public routes
|
|
35
|
-
const publicRoutes = ['/
|
|
35
|
+
const publicRoutes = ['/login', '/register'];
|
|
36
36
|
if (publicRoutes.some((route) => pathWithoutLocale.includes(route))) {
|
|
37
37
|
return intlResponse;
|
|
38
38
|
}
|
|
@@ -43,46 +43,6 @@ export default async function middleware(request: NextRequest) {
|
|
|
43
43
|
return intlResponse;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// Check today's check-in status
|
|
47
|
-
try {
|
|
48
|
-
const apiUrl =
|
|
49
|
-
process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3100/api';
|
|
50
|
-
const response = await fetch(`${apiUrl}/daily-question/streak`, {
|
|
51
|
-
headers: {
|
|
52
|
-
Authorization: `Bearer ${token}`,
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
if (response.ok) {
|
|
57
|
-
const data = await response.json();
|
|
58
|
-
const streak = data.data;
|
|
59
|
-
|
|
60
|
-
if (streak?.lastCheckInDate) {
|
|
61
|
-
const today = new Date();
|
|
62
|
-
today.setHours(0, 0, 0, 0);
|
|
63
|
-
const lastCheckIn = new Date(streak.lastCheckInDate);
|
|
64
|
-
lastCheckIn.setHours(0, 0, 0, 0);
|
|
65
|
-
|
|
66
|
-
// If not checked in today, redirect to daily challenge
|
|
67
|
-
if (lastCheckIn.getTime() !== today.getTime()) {
|
|
68
|
-
const url = request.nextUrl.clone();
|
|
69
|
-
// 保留语言前缀
|
|
70
|
-
const locale = pathname.match(/^\/(zh-CN|en)/)?.[1] || 'zh-CN';
|
|
71
|
-
url.pathname = `/${locale}/daily-challenge`;
|
|
72
|
-
return NextResponse.redirect(url);
|
|
73
|
-
}
|
|
74
|
-
} else {
|
|
75
|
-
// No check-in history, redirect to daily challenge
|
|
76
|
-
const url = request.nextUrl.clone();
|
|
77
|
-
const locale = pathname.match(/^\/(zh-CN|en)/)?.[1] || 'zh-CN';
|
|
78
|
-
url.pathname = `/${locale}/daily-challenge`;
|
|
79
|
-
return NextResponse.redirect(url);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
} catch (error) {
|
|
83
|
-
console.error('Middleware check-in status error:', error);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
46
|
return intlResponse;
|
|
87
47
|
}
|
|
88
48
|
|
|
@@ -43,14 +43,6 @@ export default {
|
|
|
43
43
|
);
|
|
44
44
|
},
|
|
45
45
|
|
|
46
|
-
escapeMongoRegexSpecialChars(inputString: string): string {
|
|
47
|
-
// MongoDB中的正则表达式特殊字符
|
|
48
|
-
const mongoRegexSpecialChars = /[\.^$*{[\]|}\\+?\-()]/g;
|
|
49
|
-
|
|
50
|
-
// 使用replace函数和回调函数来替换特殊字符
|
|
51
|
-
return inputString.replace(mongoRegexSpecialChars, (match) => `\\${match}`);
|
|
52
|
-
},
|
|
53
|
-
|
|
54
46
|
generateString(input: string, suffixLength = 3): string {
|
|
55
47
|
// 判断是否为中文
|
|
56
48
|
const isChinese = /^[\u4e00-\u9fa5]+$/.test(input);
|
|
@@ -38,6 +38,7 @@ const rl = readline.createInterface({
|
|
|
38
38
|
const question = (query) => new Promise((resolve) => rl.question(query, resolve));
|
|
39
39
|
|
|
40
40
|
// Project configuration
|
|
41
|
+
// 注意:API 端口在 apps/api/config.local.yaml 的 app.port 配置;Web 端口由 Next.js 自动配置,此处不配置
|
|
41
42
|
const config = {
|
|
42
43
|
projectName: '',
|
|
43
44
|
projectDescription: '',
|
|
@@ -48,8 +49,6 @@ const config = {
|
|
|
48
49
|
redisUrl: '',
|
|
49
50
|
rabbitmqUrl: '',
|
|
50
51
|
baseHost: '127.0.0.1',
|
|
51
|
-
apiPort: '',
|
|
52
|
-
webPort: '',
|
|
53
52
|
};
|
|
54
53
|
|
|
55
54
|
async function main() {
|
|
@@ -84,16 +83,6 @@ async function main() {
|
|
|
84
83
|
|
|
85
84
|
log.header('\n📦 Configuration');
|
|
86
85
|
|
|
87
|
-
config.apiPort = await question(
|
|
88
|
-
`${colors.cyan}API port${colors.reset} [3100]: `
|
|
89
|
-
);
|
|
90
|
-
config.apiPort = config.apiPort.trim() || '3100';
|
|
91
|
-
|
|
92
|
-
config.webPort = await question(
|
|
93
|
-
`${colors.cyan}Web port${colors.reset} [3000]: `
|
|
94
|
-
);
|
|
95
|
-
config.webPort = config.webPort.trim() || '3000';
|
|
96
|
-
|
|
97
86
|
config.databaseUrl = await question(
|
|
98
87
|
`${colors.cyan}Database URL${colors.reset} [postgresql://user:password@localhost:5432/dbname]: `
|
|
99
88
|
);
|
|
@@ -126,8 +115,6 @@ async function main() {
|
|
|
126
115
|
console.log(` Project Name: ${colors.green}${config.projectName}${colors.reset}`);
|
|
127
116
|
console.log(` Description: ${config.projectDescription}`);
|
|
128
117
|
console.log(` Author: ${config.authorName} <${config.authorEmail}>`);
|
|
129
|
-
console.log(` API Port: ${config.apiPort}`);
|
|
130
|
-
console.log(` Web Port: ${config.webPort}`);
|
|
131
118
|
console.log(` Base Host: ${config.baseHost}`);
|
|
132
119
|
console.log(` Database: ${config.databaseUrl}`);
|
|
133
120
|
console.log(` Read DB: ${config.readDatabaseUrl}`);
|
|
@@ -237,14 +224,13 @@ function createEnvFromExample(rootDir, config) {
|
|
|
237
224
|
const webExamplePath = path.join(rootDir, 'apps/web/.env.example');
|
|
238
225
|
|
|
239
226
|
// 变量替换映射(apps/api),参考 apps/api/.env.example
|
|
227
|
+
// API 端口由 apps/api/config.local.yaml 的 app.port 配置,此处不覆盖 API_BASE_URL / INTERNAL_API_BASE_URL
|
|
240
228
|
const apiReplacements = {
|
|
241
229
|
BASE_HOST: config.baseHost,
|
|
242
230
|
DATABASE_URL: config.databaseUrl,
|
|
243
231
|
READ_DATABASE_URL: config.readDatabaseUrl,
|
|
244
232
|
REDIS_URL: config.redisUrl,
|
|
245
233
|
RABBITMQ_URL: config.rabbitmqUrl,
|
|
246
|
-
API_BASE_URL: `http://localhost:${config.apiPort}/api`,
|
|
247
|
-
INTERNAL_API_BASE_URL: `http://127.0.0.1:${config.apiPort}/api`,
|
|
248
234
|
};
|
|
249
235
|
|
|
250
236
|
if (fs.existsSync(apiExamplePath)) {
|
|
@@ -281,15 +267,11 @@ function createEnvFromExample(rootDir, config) {
|
|
|
281
267
|
REDIS_URL: config.redisUrl,
|
|
282
268
|
RABBITMQ_URL: config.rabbitmqUrl,
|
|
283
269
|
JWT_SECRET: generateRandomSecret(),
|
|
284
|
-
API_BASE_URL: `http://localhost:${config.apiPort}/api`,
|
|
285
|
-
INTERNAL_API_BASE_URL: `http://127.0.0.1:${config.apiPort}/api`,
|
|
286
270
|
});
|
|
287
271
|
}
|
|
288
272
|
|
|
289
|
-
// apps/web
|
|
290
|
-
const webReplacements = {
|
|
291
|
-
NEXT_PUBLIC_API_BASE_URL: `http://localhost:${config.apiPort}/api`,
|
|
292
|
-
};
|
|
273
|
+
// apps/web:NEXT_PUBLIC_API_BASE_URL 等由 .env.example 提供,Next.js 端口自动配置,此处不覆盖
|
|
274
|
+
const webReplacements = {};
|
|
293
275
|
|
|
294
276
|
if (fs.existsSync(webExamplePath)) {
|
|
295
277
|
const content = fs.readFileSync(webExamplePath, 'utf8');
|
|
@@ -309,9 +291,7 @@ function createEnvFromExample(rootDir, config) {
|
|
|
309
291
|
outLines.join('\n') + '\n'
|
|
310
292
|
);
|
|
311
293
|
} else {
|
|
312
|
-
createEnvFile(path.join(rootDir, 'apps/web/.env.local'), {
|
|
313
|
-
NEXT_PUBLIC_API_BASE_URL: `http://localhost:${config.apiPort}/api`,
|
|
314
|
-
});
|
|
294
|
+
createEnvFile(path.join(rootDir, 'apps/web/.env.local'), {});
|
|
315
295
|
}
|
|
316
296
|
}
|
|
317
297
|
|