@fecommunity/reactpress-theme-starter 1.0.0-beta.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.
Files changed (163) hide show
  1. package/.env.example +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +286 -0
  4. package/app/api/[[...path]]/route.ts +69 -0
  5. package/app/archives/page.tsx +28 -0
  6. package/app/article/[id]/page.tsx +92 -0
  7. package/app/category/[category]/page.tsx +60 -0
  8. package/app/globals.css +4969 -0
  9. package/app/knowledge/[pId]/[id]/page.tsx +54 -0
  10. package/app/knowledge/[pId]/page.tsx +47 -0
  11. package/app/knowledge/page.tsx +29 -0
  12. package/app/layout.tsx +76 -0
  13. package/app/llms.txt/route.ts +26 -0
  14. package/app/login/page.tsx +17 -0
  15. package/app/nav/[id]/page.tsx +44 -0
  16. package/app/nav/page.tsx +5 -0
  17. package/app/not-found.tsx +21 -0
  18. package/app/page/[id]/page.tsx +57 -0
  19. package/app/page.tsx +61 -0
  20. package/app/register/page.tsx +17 -0
  21. package/app/robots.ts +14 -0
  22. package/app/rss/route.ts +82 -0
  23. package/app/search/page.tsx +46 -0
  24. package/app/sitemap.ts +38 -0
  25. package/app/suggestions/page.tsx +47 -0
  26. package/app/tag/[tag]/page.tsx +58 -0
  27. package/app/tags/page.tsx +18 -0
  28. package/app/template.tsx +6 -0
  29. package/components/article/ArchiveBanner.tsx +40 -0
  30. package/components/article/ArticleActionsBar.tsx +53 -0
  31. package/components/article/ArticleCarousel.tsx +237 -0
  32. package/components/article/ArticleCopyrightBar.tsx +128 -0
  33. package/components/article/ArticleEndBar.tsx +19 -0
  34. package/components/article/ArticleFeedSection.tsx +59 -0
  35. package/components/article/ArticleList.tsx +170 -0
  36. package/components/article/ArticleReadingChrome.tsx +104 -0
  37. package/components/article/ArticleReadingProgress.tsx +75 -0
  38. package/components/article/ArticleRecommend.tsx +362 -0
  39. package/components/article/ArticleTocPanel.tsx +19 -0
  40. package/components/article/ArticleViewClient.tsx +221 -0
  41. package/components/article/CategoryMenu.tsx +67 -0
  42. package/components/article/CmsPageClient.tsx +92 -0
  43. package/components/article/LoadMore.tsx +59 -0
  44. package/components/article/ReadingContent.tsx +51 -0
  45. package/components/auth/AuthPageClient.tsx +314 -0
  46. package/components/comment/CommentEditor.tsx +290 -0
  47. package/components/comment/CommentEmoji.tsx +110 -0
  48. package/components/comment/CommentIconButton.tsx +37 -0
  49. package/components/comment/CommentItem.tsx +135 -0
  50. package/components/comment/CommentSection.tsx +123 -0
  51. package/components/comment/emojis.ts +152 -0
  52. package/components/knowledge/KnowledgeList.tsx +70 -0
  53. package/components/layout/BackToTop.tsx +27 -0
  54. package/components/layout/ConditionalSiteFooter.tsx +29 -0
  55. package/components/layout/ContentStylesLoader.tsx +9 -0
  56. package/components/layout/DoubleColumnLayout.tsx +171 -0
  57. package/components/layout/HeaderSearch.tsx +23 -0
  58. package/components/layout/LocaleSwitcher.tsx +24 -0
  59. package/components/layout/PageContainer.tsx +11 -0
  60. package/components/layout/PageLoadProgress.tsx +169 -0
  61. package/components/layout/SiteFooter.tsx +75 -0
  62. package/components/layout/SiteHeader.tsx +312 -0
  63. package/components/layout/SystemNotification.tsx +176 -0
  64. package/components/layout/ThemeSwitch.tsx +19 -0
  65. package/components/layout/UserAuth.tsx +130 -0
  66. package/components/search/SearchHero.tsx +323 -0
  67. package/components/search/SearchQuickLinks.tsx +285 -0
  68. package/components/shared/DevChunkRecovery.tsx +8 -0
  69. package/components/shared/Link.tsx +30 -0
  70. package/components/shared/SectionHeading.tsx +38 -0
  71. package/components/shared/ThemeImage.tsx +13 -0
  72. package/components/shared/logo.svg +7 -0
  73. package/components/views/ArchivesClient.tsx +137 -0
  74. package/components/views/CategoryClient.tsx +87 -0
  75. package/components/views/HomeClient.tsx +79 -0
  76. package/components/views/KnowledgeBookClient.tsx +127 -0
  77. package/components/views/KnowledgeChapterClient.tsx +175 -0
  78. package/components/views/KnowledgeClient.tsx +69 -0
  79. package/components/views/LoginClient.tsx +80 -0
  80. package/components/views/NavDetailClient.tsx +177 -0
  81. package/components/views/SearchClient.tsx +100 -0
  82. package/components/views/TagArchiveClient.tsx +105 -0
  83. package/components/views/TagsClient.tsx +46 -0
  84. package/components/widgets/AboutUs.tsx +180 -0
  85. package/components/widgets/ArticleActionButton.tsx +95 -0
  86. package/components/widgets/BookmarkWidget.tsx +61 -0
  87. package/components/widgets/CategoriesWidget.tsx +47 -0
  88. package/components/widgets/HomeSidebar.tsx +40 -0
  89. package/components/widgets/LikesWidget.tsx +77 -0
  90. package/components/widgets/ShareWidget.tsx +120 -0
  91. package/components/widgets/TagCloud.tsx +94 -0
  92. package/components/widgets/TagsWidget.tsx +160 -0
  93. package/eslint.config.mjs +80 -0
  94. package/lib/auth/githubOAuth.ts +26 -0
  95. package/lib/auth/sessionUser.ts +41 -0
  96. package/lib/mock-api/auth.ts +129 -0
  97. package/lib/mock-api/config.ts +11 -0
  98. package/lib/mock-api/cors.ts +53 -0
  99. package/lib/mock-api/data.ts +998 -0
  100. package/lib/mock-api/match-route.ts +202 -0
  101. package/lib/mock-api/proxy.ts +28 -0
  102. package/lib/providers/client.ts +58 -0
  103. package/lib/providers/materializeProvider.ts +9 -0
  104. package/lib/providers/server.ts +8 -0
  105. package/lib/reactpress/appearance.ts +69 -0
  106. package/lib/reactpress/bootstrap.ts +48 -0
  107. package/lib/reactpress/coercePublishedPage.ts +12 -0
  108. package/lib/reactpress/contentSeo.ts +123 -0
  109. package/lib/reactpress/env.ts +31 -0
  110. package/lib/reactpress/feedFooterPlacement.ts +16 -0
  111. package/lib/reactpress/fetchSetting.ts +19 -0
  112. package/lib/reactpress/homeFooter.tsx +27 -0
  113. package/lib/reactpress/layout-shell.tsx +19 -0
  114. package/lib/reactpress/materializeBootstrapCatalog.ts +31 -0
  115. package/lib/reactpress/normalizeApiResponse.ts +25 -0
  116. package/lib/reactpress/providers.tsx +199 -0
  117. package/lib/reactpress/resolveSiteNav.ts +56 -0
  118. package/lib/reactpress/seoMetadata.ts +82 -0
  119. package/lib/reactpress/serverLocale.ts +43 -0
  120. package/lib/reactpress/siteMetadata.ts +151 -0
  121. package/lib/reactpress/staticParams.ts +46 -0
  122. package/lib/reactpress/suggestionsContent.ts +48 -0
  123. package/lib/reactpress/useFeedFooterPlacement.ts +22 -0
  124. package/lib/reactpress/useSiteNavConfig.ts +39 -0
  125. package/lib/utils/archiveBanner.ts +8 -0
  126. package/lib/utils/colors.ts +119 -0
  127. package/lib/utils/comment.ts +46 -0
  128. package/lib/utils/headerPosition.ts +55 -0
  129. package/lib/utils/icons.tsx +238 -0
  130. package/lib/utils/reading.ts +16 -0
  131. package/lib/utils/scroll.ts +8 -0
  132. package/lib/utils/tagCloud.ts +222 -0
  133. package/next.config.js +103 -0
  134. package/package.json +98 -0
  135. package/postcss.config.js +5 -0
  136. package/prettier.config.js +10 -0
  137. package/public/apple-touch-icon.png +0 -0
  138. package/public/cli.png +0 -0
  139. package/public/favicon-32.png +0 -0
  140. package/public/favicon.ico +0 -0
  141. package/public/home-dark.png +0 -0
  142. package/public/home-light.png +0 -0
  143. package/public/icon-192.png +0 -0
  144. package/public/icon-512.png +0 -0
  145. package/public/logo-400.png +0 -0
  146. package/public/logo.png +0 -0
  147. package/public/mobile.png +0 -0
  148. package/public/pad.png +0 -0
  149. package/public/site.webmanifest +19 -0
  150. package/scripts/build-mock.mjs +139 -0
  151. package/scripts/dev.mjs +221 -0
  152. package/scripts/ensure-env.mjs +100 -0
  153. package/scripts/remote-dev.mjs +143 -0
  154. package/scripts/resolve-api-env.mjs +114 -0
  155. package/scripts/set-publish-version.mjs +31 -0
  156. package/scripts/smoke-pages.mjs +113 -0
  157. package/scripts/start-mock.mjs +79 -0
  158. package/scripts/verify-toolkit.mjs +37 -0
  159. package/svg.d.ts +6 -0
  160. package/theme.json +123 -0
  161. package/theme.manifest.schema.json +265 -0
  162. package/tsconfig.json +41 -0
  163. package/vercel.json +12 -0
@@ -0,0 +1,222 @@
1
+ interface TagParams {
2
+ radius?: number
3
+ d?: number
4
+ dtr?: number
5
+ distr?: boolean
6
+ tSpeed?: number
7
+ size?: number
8
+ }
9
+
10
+ interface TagMeta {
11
+ on?: boolean
12
+ offsetWidth: number
13
+ offsetHeight: number
14
+ cx: number
15
+ cy: number
16
+ cz: number
17
+ x: number
18
+ y: number
19
+ scale: number
20
+ alpha: number
21
+ zIndex: number
22
+ }
23
+
24
+ export default class TagCloudEngine {
25
+ private oDiv?: HTMLDivElement
26
+ private aA: HTMLCollectionOf<HTMLAnchorElement> | null = null
27
+ private sa = 0
28
+ private ca = 0
29
+ private sb = 0
30
+ private cb = 0
31
+ private sc = 0
32
+ private cc = 0
33
+
34
+ protected radius = 90
35
+ protected d = 200
36
+ protected dtr = Math.PI / 180
37
+ protected mcList: TagMeta[] = []
38
+ protected distr = true
39
+ protected tSpeed = 11
40
+ protected size = 200
41
+ protected readonly mouseX = 0
42
+ protected readonly mouseY = 10
43
+ protected readonly howElliptical = 1
44
+ private rafId: number | null = null
45
+
46
+ constructor(params?: TagParams) {
47
+ if (params?.radius != null) this.radius = params.radius
48
+ if (params?.d != null) this.d = params.d
49
+ if (params?.dtr != null) this.dtr = params.dtr
50
+ if (params?.distr != null) this.distr = params.distr
51
+ if (params?.tSpeed != null) this.tSpeed = params.tSpeed
52
+ if (params?.size != null) this.size = params.size
53
+ }
54
+
55
+ destroy = () => {
56
+ if (this.rafId != null) {
57
+ cancelAnimationFrame(this.rafId)
58
+ this.rafId = null
59
+ }
60
+ this.mcList = []
61
+ this.aA = null
62
+ }
63
+
64
+ init = (container?: HTMLDivElement | string) => {
65
+ this.destroy()
66
+
67
+ if (container instanceof HTMLDivElement) {
68
+ this.oDiv = container
69
+ } else if (typeof container === 'string') {
70
+ this.oDiv = document.querySelector(container) as HTMLDivElement | null
71
+ }
72
+
73
+ if (!this.oDiv) return
74
+
75
+ this.aA = this.oDiv.getElementsByTagName('a')
76
+ if (!this.aA.length) return
77
+
78
+ for (let i = 0; i < this.aA.length; i++) {
79
+ const el = this.aA[i]
80
+ const meta: TagMeta = {
81
+ offsetWidth: el.offsetWidth,
82
+ offsetHeight: el.offsetHeight,
83
+ cx: 0,
84
+ cy: 0,
85
+ cz: 0,
86
+ x: 0,
87
+ y: 0,
88
+ scale: 1,
89
+ alpha: 1,
90
+ zIndex: 0,
91
+ }
92
+
93
+ el.onmouseover = function (this: HTMLAnchorElement) {
94
+ meta.on = true
95
+ this.style.zIndex = '9999'
96
+ this.style.color = '#fff'
97
+ this.style.padding = '5px 10px'
98
+ this.style.opacity = '1'
99
+ }
100
+
101
+ el.onmouseout = function (this: HTMLAnchorElement) {
102
+ meta.on = false
103
+ this.style.zIndex = String(meta.zIndex)
104
+ this.style.padding = '5px 8px'
105
+ this.style.opacity = '1'
106
+ }
107
+
108
+ this.mcList.push(meta)
109
+ }
110
+
111
+ this.sineCosine(0, 0, 0)
112
+ this.positionAll()
113
+ this.doPosition()
114
+ this.rafId = requestAnimationFrame(this.update)
115
+ }
116
+
117
+ private update = () => {
118
+ const a = (Math.min(Math.max(-this.mouseY, -this.size), this.size) / this.radius) * this.tSpeed
119
+ const b = (-Math.min(Math.max(-this.mouseX, -this.size), this.size) / this.radius) * this.tSpeed
120
+
121
+ if (Math.abs(a) > 0.01 || Math.abs(b) > 0.01) {
122
+ this.sineCosine(a, b, 0)
123
+
124
+ for (let i = 0; i < this.mcList.length; i++) {
125
+ if (this.mcList[i].on) continue
126
+
127
+ const rx1 = this.mcList[i].cx
128
+ const ry1 = this.mcList[i].cy * this.ca + this.mcList[i].cz * -this.sa
129
+ const rz1 = this.mcList[i].cy * this.sa + this.mcList[i].cz * this.ca
130
+
131
+ const rx2 = rx1 * this.cb + rz1 * this.sb
132
+ const ry2 = ry1
133
+ const rz2 = rx1 * -this.sb + rz1 * this.cb
134
+
135
+ const rx3 = rx2 * this.cc + ry2 * -this.sc
136
+ const ry3 = rx2 * this.sc + ry2 * this.cc
137
+ const rz3 = rz2
138
+
139
+ this.mcList[i].cx = rx3
140
+ this.mcList[i].cy = ry3
141
+ this.mcList[i].cz = rz3
142
+
143
+ const per = this.d / (this.d + rz3)
144
+ this.mcList[i].x = this.howElliptical * rx3 * per - this.howElliptical * 2
145
+ this.mcList[i].y = ry3 * per
146
+ this.mcList[i].scale = per
147
+
148
+ let alpha = per
149
+ alpha = (alpha - 0.6) * (10 / 6)
150
+ this.mcList[i].alpha = alpha * alpha * alpha - 0.2
151
+ this.mcList[i].zIndex = Math.ceil(100 - Math.floor(this.mcList[i].cz))
152
+ }
153
+
154
+ this.doPosition()
155
+ }
156
+
157
+ this.rafId = requestAnimationFrame(this.update)
158
+ }
159
+
160
+ private positionAll = () => {
161
+ const max = Math.min(this.mcList.length, this.aA?.length ?? 0)
162
+ if (!max || !this.oDiv) return
163
+
164
+ for (let i = 0; i < max; i++) {
165
+ const el = this.aA?.[i]
166
+ if (!el) continue
167
+
168
+ let phi = 0
169
+ let theta = 0
170
+ if (this.distr) {
171
+ phi = Math.acos(-1 + (2 * (i + 1) - 1) / max)
172
+ theta = Math.sqrt(max * Math.PI) * phi
173
+ } else {
174
+ phi = Math.random() * Math.PI
175
+ theta = Math.random() * (2 * Math.PI)
176
+ }
177
+
178
+ this.mcList[i].cx = this.radius * Math.cos(theta) * Math.sin(phi)
179
+ this.mcList[i].cy = this.radius * Math.sin(theta) * Math.sin(phi)
180
+ this.mcList[i].cz = this.radius * Math.cos(phi)
181
+
182
+ el.style.left = `${this.mcList[i].cx + this.oDiv.offsetWidth / 2 - this.mcList[i].offsetWidth / 2}px`
183
+ el.style.top = `${this.mcList[i].cy + this.oDiv.offsetHeight / 2 - this.mcList[i].offsetHeight / 2}px`
184
+ }
185
+ }
186
+
187
+ private doPosition = () => {
188
+ if (!this.oDiv) return
189
+
190
+ const l = this.oDiv.offsetWidth / 2
191
+ const t = this.oDiv.offsetHeight / 2
192
+ const count = Math.min(this.mcList.length, this.aA?.length ?? 0)
193
+
194
+ for (let i = 0; i < count; i++) {
195
+ if (this.mcList[i].on) continue
196
+
197
+ const el = this.aA?.[i]
198
+ if (!el) continue
199
+
200
+ if (this.mcList[i].alpha > 0.1) {
201
+ el.style.display = ''
202
+ } else {
203
+ el.style.display = 'none'
204
+ continue
205
+ }
206
+
207
+ el.style.left = `${this.mcList[i].cx + l - this.mcList[i].offsetWidth / 2}px`
208
+ el.style.top = `${this.mcList[i].cy + t - this.mcList[i].offsetHeight / 2}px`
209
+ el.style.opacity = '1'
210
+ el.style.zIndex = String(this.mcList[i].zIndex)
211
+ }
212
+ }
213
+
214
+ private sineCosine = (a: number, b: number, c: number) => {
215
+ this.sa = Math.sin(a * this.dtr)
216
+ this.ca = Math.cos(a * this.dtr)
217
+ this.sb = Math.sin(b * this.dtr)
218
+ this.cb = Math.cos(b * this.dtr)
219
+ this.sc = Math.sin(c * this.dtr)
220
+ this.cc = Math.cos(c * this.dtr)
221
+ }
222
+ }
package/next.config.js ADDED
@@ -0,0 +1,103 @@
1
+ const path = require('path')
2
+ const { config } = require('@fecommunity/reactpress-toolkit/config')
3
+ const {
4
+ resolveThemeApiEnv,
5
+ resolveApiRewriteOrigin,
6
+ resolveClientSiteUrl,
7
+ isLocalhostUrl,
8
+ } = require('./scripts/resolve-api-env.mjs')
9
+
10
+ // App Router (Next 15): do not use createReactPressNextConfig's react webpack alias —
11
+ // it conflicts with Next's compiled React and causes SSR "useContext" null errors.
12
+ // Server code imports `@fecommunity/reactpress-toolkit/theme/server` (no UI barrel).
13
+
14
+ const themeApiEnv = resolveThemeApiEnv()
15
+
16
+ function resolveUploadRewriteOrigin() {
17
+ if (process.env.REACTPRESS_MOCK_API === '1') {
18
+ const site = resolveClientSiteUrl()
19
+ if (site && !isLocalhostUrl(site)) return site
20
+ }
21
+ return resolveApiRewriteOrigin(themeApiEnv)
22
+ }
23
+
24
+ const uploadRewriteOrigin = resolveUploadRewriteOrigin()
25
+
26
+ /** Only inline browser-safe env — server API URLs must stay runtime on Vercel. */
27
+ const publicThemeEnv = {
28
+ NEXT_PUBLIC_REACTPRESS_API_URL: themeApiEnv.NEXT_PUBLIC_REACTPRESS_API_URL,
29
+ NEXT_PUBLIC_REACTPRESS_ADMIN_URL: themeApiEnv.NEXT_PUBLIC_REACTPRESS_ADMIN_URL,
30
+ CLIENT_SITE_URL: themeApiEnv.CLIENT_SITE_URL,
31
+ }
32
+
33
+ const basePath = process.env.BASE_PATH || undefined
34
+ const output = process.env.EXPORT ? 'export' : undefined
35
+
36
+ /** @type {import('next').NextConfig} */
37
+ module.exports = {
38
+ output,
39
+ basePath,
40
+ distDir: process.env.NEXT_DIST_DIR || '.next',
41
+ outputFileTracingRoot: path.join(__dirname),
42
+ ...(config.CLIENT_ASSET_PREFIX ? { assetPrefix: config.CLIENT_ASSET_PREFIX } : {}),
43
+ poweredByHeader: false,
44
+ reactStrictMode: true,
45
+ trailingSlash: true,
46
+ skipTrailingSlashRedirect: true,
47
+ transpilePackages: ['@fecommunity/reactpress-toolkit'],
48
+ experimental: {
49
+ optimizePackageImports: [
50
+ '@fecommunity/reactpress-toolkit',
51
+ '@fecommunity/reactpress-toolkit/ui',
52
+ ],
53
+ // Avoid SegmentViewNode manifest corruption in dev (Next 15.5+).
54
+ devtoolSegmentExplorer: false,
55
+ },
56
+ eslint: { ignoreDuringBuilds: process.env.ENFORCE_ESLINT === '1' ? false : true },
57
+ typescript: { ignoreBuildErrors: process.env.ENFORCE_TYPECHECK === '1' ? false : true },
58
+ env: {
59
+ ...publicThemeEnv,
60
+ },
61
+ images: {
62
+ remotePatterns: [
63
+ { protocol: 'http', hostname: 'localhost' },
64
+ { protocol: 'https', hostname: '**' },
65
+ ],
66
+ unoptimized: process.env.UNOPTIMIZED ? true : undefined,
67
+ },
68
+ webpack: (webpackConfig) => {
69
+ webpackConfig.module.rules.push({
70
+ test: /\.svg$/,
71
+ use: ['@svgr/webpack'],
72
+ })
73
+ return webpackConfig
74
+ },
75
+ async rewrites() {
76
+ return [{ source: '/uploads/:path*', destination: `${uploadRewriteOrigin}/uploads/:path*` }]
77
+ },
78
+ async redirects() {
79
+ return [
80
+ { source: '/blog', destination: '/', permanent: true },
81
+ { source: '/blog/', destination: '/', permanent: true },
82
+ { source: '/tags/:tag', destination: '/tag/:tag', permanent: true },
83
+ { source: '/tags/:tag/page/:page', destination: '/tag/:tag', permanent: true },
84
+ { source: '/page/suggestions', destination: '/suggestions', permanent: true },
85
+ { source: '/page/suggestions/', destination: '/suggestions/', permanent: true },
86
+ { source: '/nav', destination: '/search', permanent: true },
87
+ { source: '/nav/', destination: '/search/', permanent: true },
88
+ ]
89
+ },
90
+ async headers() {
91
+ return [
92
+ {
93
+ source: '/:path*',
94
+ headers: [
95
+ { key: 'X-Content-Type-Options', value: 'nosniff' },
96
+ { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
97
+ { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
98
+ { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
99
+ ],
100
+ },
101
+ ]
102
+ },
103
+ }
package/package.json ADDED
@@ -0,0 +1,98 @@
1
+ {
2
+ "name": "@fecommunity/reactpress-theme-starter",
3
+ "version": "1.0.0-beta.0",
4
+ "description": "ReactPress 博客主题 Starter:Tailwind CSS + Next.js App Router,对接 toolkit 动态内容",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "reactpress",
8
+ "nextjs",
9
+ "blog",
10
+ "theme",
11
+ "tailwindcss",
12
+ "headless-cms"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/fecommunity/reactpress-theme-starter.git"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/fecommunity/reactpress-theme-starter/issues"
20
+ },
21
+ "homepage": "https://github.com/fecommunity/reactpress-theme-starter#readme",
22
+ "publishConfig": {
23
+ "access": "public",
24
+ "registry": "https://registry.npmjs.org/"
25
+ },
26
+ "files": [
27
+ "app",
28
+ "components",
29
+ "lib",
30
+ "public",
31
+ "scripts",
32
+ "theme.json",
33
+ "theme.manifest.schema.json",
34
+ "next.config.js",
35
+ "postcss.config.js",
36
+ "prettier.config.js",
37
+ "eslint.config.mjs",
38
+ "tsconfig.json",
39
+ "svg.d.ts",
40
+ "vercel.json",
41
+ ".env.example"
42
+ ],
43
+ "packageManager": "pnpm@9.15.9",
44
+ "engines": {
45
+ "node": ">=20"
46
+ },
47
+ "scripts": {
48
+ "dev": "node scripts/dev.mjs",
49
+ "dev:mock": "cross-env REACTPRESS_MOCK_API=1 node scripts/dev.mjs",
50
+ "clean": "rm -rf .next .next-dev",
51
+ "build": "cross-env INIT_CWD=$PWD next build",
52
+ "build:mock": "node scripts/build-mock.mjs",
53
+ "build:ci": "pnpm clean && cross-env INIT_CWD=$PWD next build",
54
+ "start": "cross-env INIT_CWD=$PWD next start -p 3001",
55
+ "start:mock": "node scripts/start-mock.mjs",
56
+ "lint": "eslint . --fix",
57
+ "lint:check": "eslint .",
58
+ "typecheck": "tsc --noEmit",
59
+ "format": "prettier --write .",
60
+ "format:check": "prettier --check .",
61
+ "check": "pnpm run lint:check && pnpm run format:check",
62
+ "postinstall": "node scripts/verify-toolkit.mjs && node scripts/ensure-env.mjs"
63
+ },
64
+ "dependencies": {
65
+ "@fecommunity/reactpress-toolkit": "^3.1.0",
66
+ "@tailwindcss/forms": "^0.5.11",
67
+ "@tailwindcss/postcss": "^4.1.18",
68
+ "@tailwindcss/typography": "^0.5.19",
69
+ "cross-env": "^10.1.0",
70
+ "highlight.js": "^9.18.5",
71
+ "next": "15.5.12",
72
+ "nprogress": "^0.2.0",
73
+ "postcss": "^8.5.6",
74
+ "react": "19.2.4",
75
+ "react-dom": "19.2.4",
76
+ "tailwindcss": "^4.1.18",
77
+ "viewerjs": "^1.5.0"
78
+ },
79
+ "devDependencies": {
80
+ "@eslint/eslintrc": "^3.3.3",
81
+ "@eslint/js": "^9.18.0",
82
+ "@svgr/webpack": "^8.1.0",
83
+ "@types/node": "^20.17.0",
84
+ "@types/react": "^19.2.13",
85
+ "@types/react-dom": "^19.2.3",
86
+ "@typescript-eslint/eslint-plugin": "^8.54.0",
87
+ "@typescript-eslint/parser": "^8.54.0",
88
+ "eslint": "^9.18.0",
89
+ "eslint-config-next": "15.5.12",
90
+ "eslint-config-prettier": "^10.1.8",
91
+ "eslint-plugin-jsx-a11y": "^6.10.2",
92
+ "eslint-plugin-prettier": "^5.5.5",
93
+ "globals": "^17.3.0",
94
+ "prettier": "^3.8.1",
95
+ "prettier-plugin-tailwindcss": "^0.7.2",
96
+ "typescript": "^5.9.3"
97
+ }
98
+ }
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ plugins: {
3
+ '@tailwindcss/postcss': {},
4
+ },
5
+ }
@@ -0,0 +1,10 @@
1
+ module.exports = {
2
+ semi: false,
3
+ singleQuote: true,
4
+ printWidth: 100,
5
+ tabWidth: 2,
6
+ useTabs: false,
7
+ trailingComma: 'es5',
8
+ bracketSpacing: true,
9
+ plugins: ['prettier-plugin-tailwindcss'],
10
+ }
Binary file
package/public/cli.png ADDED
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/public/pad.png ADDED
Binary file
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "ReactPress",
3
+ "short_name": "ReactPress",
4
+ "icons": [
5
+ {
6
+ "src": "/icon-192.png",
7
+ "sizes": "192x192",
8
+ "type": "image/png"
9
+ },
10
+ {
11
+ "src": "/icon-512.png",
12
+ "sizes": "512x512",
13
+ "type": "image/png"
14
+ }
15
+ ],
16
+ "theme_color": "#087ea4",
17
+ "background_color": "#ffffff",
18
+ "display": "standalone"
19
+ }
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Production build against the in-process mock API (same data as `pnpm dev:mock`).
4
+ *
5
+ * Used by Vercel and CI: start `next dev` with mock routes, wait until ready,
6
+ * then run `next build` so SSR/ISR prefetch uses mock data instead of a remote API.
7
+ */
8
+ import { spawn } from 'node:child_process'
9
+ import path from 'node:path'
10
+ import { fileURLToPath } from 'node:url'
11
+ import { ensureEnvFile } from './ensure-env.mjs'
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
14
+ const projectRoot = path.resolve(__dirname, '..')
15
+
16
+ const MOCK_PORT = Number(process.env.REACTPRESS_MOCK_BUILD_PORT) || 3010
17
+ const MOCK_HOST = process.env.REACTPRESS_MOCK_BUILD_HOST || '127.0.0.1'
18
+ const MOCK_API = `http://${MOCK_HOST}:${MOCK_PORT}/api`
19
+ const DEV_DIST_DIR = process.env.NEXT_DIST_DIR || '.next-dev'
20
+
21
+ function run(command, args, env = process.env) {
22
+ return new Promise((resolve, reject) => {
23
+ const child = spawn(command, args, {
24
+ cwd: projectRoot,
25
+ env,
26
+ stdio: 'inherit',
27
+ shell: process.platform === 'win32',
28
+ })
29
+
30
+ child.on('error', reject)
31
+ child.on('exit', (code, signal) => {
32
+ if (code === 0) resolve()
33
+ else reject(new Error(`${command} exited with code ${code ?? signal}`))
34
+ })
35
+ })
36
+ }
37
+
38
+ async function waitForMockApi() {
39
+ const healthUrl = `${MOCK_API}/setting/get`
40
+ const maxAttempts = 90
41
+
42
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
43
+ try {
44
+ const response = await fetch(healthUrl, { method: 'POST' })
45
+ if (response.ok) {
46
+ console.log(`[build:mock] Mock API ready at ${MOCK_API}`)
47
+ return
48
+ }
49
+ } catch {
50
+ // dev server still starting
51
+ }
52
+
53
+ await new Promise((resolve) => setTimeout(resolve, 1000))
54
+ }
55
+
56
+ throw new Error(`Mock API failed to start at ${MOCK_API}`)
57
+ }
58
+
59
+ async function main() {
60
+ const clientSiteUrl =
61
+ process.env.CLIENT_SITE_URL?.trim() || 'https://reactpress-theme-starter.vercel.app'
62
+
63
+ ensureEnvFile(projectRoot, {
64
+ overrides: {
65
+ REACTPRESS_MOCK_API: '1',
66
+ REACTPRESS_API_URL: MOCK_API,
67
+ SERVER_API_URL: MOCK_API,
68
+ NEXT_PUBLIC_REACTPRESS_API_URL: MOCK_API,
69
+ CLIENT_SITE_URL: clientSiteUrl,
70
+ },
71
+ })
72
+
73
+ const devEnv = {
74
+ ...process.env,
75
+ INIT_CWD: projectRoot,
76
+ REACTPRESS_ORIGINAL_CWD: projectRoot,
77
+ NEXT_DIST_DIR: DEV_DIST_DIR,
78
+ REACTPRESS_MOCK_API: '1',
79
+ REACTPRESS_API_URL: MOCK_API,
80
+ SERVER_API_URL: MOCK_API,
81
+ NEXT_PUBLIC_REACTPRESS_API_URL: MOCK_API,
82
+ NEXT_TELEMETRY_DISABLED: '1',
83
+ }
84
+
85
+ console.log(`[build:mock] Starting mock dev server on ${MOCK_HOST}:${MOCK_PORT}`)
86
+ const devProcess = spawn(
87
+ 'pnpm',
88
+ ['exec', 'next', 'dev', '-p', String(MOCK_PORT), '-H', MOCK_HOST],
89
+ {
90
+ cwd: projectRoot,
91
+ env: devEnv,
92
+ stdio: 'inherit',
93
+ shell: process.platform === 'win32',
94
+ detached: process.platform !== 'win32',
95
+ }
96
+ )
97
+
98
+ const stopDev = () => {
99
+ if (devProcess.killed) return
100
+ if (process.platform === 'win32') {
101
+ devProcess.kill()
102
+ return
103
+ }
104
+ try {
105
+ process.kill(-devProcess.pid, 'SIGTERM')
106
+ } catch {
107
+ devProcess.kill('SIGTERM')
108
+ }
109
+ }
110
+
111
+ process.on('SIGINT', stopDev)
112
+ process.on('SIGTERM', stopDev)
113
+
114
+ try {
115
+ await waitForMockApi()
116
+
117
+ const buildEnv = {
118
+ ...process.env,
119
+ INIT_CWD: projectRoot,
120
+ VERCEL: '1',
121
+ REACTPRESS_MOCK_API: '1',
122
+ REACTPRESS_API_URL: MOCK_API,
123
+ SERVER_API_URL: MOCK_API,
124
+ CLIENT_SITE_URL: clientSiteUrl,
125
+ // Client bundle must use same-origin /api at runtime (not build-time localhost).
126
+ NEXT_PUBLIC_REACTPRESS_API_URL: '/api',
127
+ }
128
+
129
+ console.log('[build:mock] Running production build')
130
+ await run('pnpm', ['exec', 'next', 'build'], buildEnv)
131
+ } finally {
132
+ stopDev()
133
+ }
134
+ }
135
+
136
+ main().catch((error) => {
137
+ console.error('[build:mock]', error.message || error)
138
+ process.exit(1)
139
+ })