create-momentum-app 0.5.1 → 0.5.3
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/index.cjs +23 -10
- package/package.json +2 -1
- package/templates/analog/src/momentum.config.ts.tmpl +6 -6
- package/templates/angular/src/momentum.config.ts.tmpl +6 -6
- package/templates/nestjs/angular.json.tmpl +73 -0
- package/templates/nestjs/eslint.config.mjs +31 -0
- package/templates/nestjs/package.json.tmpl +69 -0
- package/templates/nestjs/postcss.config.js +6 -0
- package/templates/nestjs/src/app/app.config.server.ts +10 -0
- package/templates/nestjs/src/app/app.config.ts +18 -0
- package/templates/nestjs/src/app/app.routes.server.ts +8 -0
- package/templates/nestjs/src/app/app.routes.ts +22 -0
- package/templates/nestjs/src/app/app.ts +9 -0
- package/templates/nestjs/src/app/pages/blocks/hero-block.component.ts +41 -0
- package/templates/nestjs/src/app/pages/blocks/image-text-block.component.ts +56 -0
- package/templates/nestjs/src/app/pages/blocks/text-block.component.ts +28 -0
- package/templates/nestjs/src/app/pages/post-block-providers.ts +17 -0
- package/templates/nestjs/src/app/pages/post-detail.resolver.ts +31 -0
- package/templates/nestjs/src/app/pages/post-detail.ts +139 -0
- package/templates/nestjs/src/app/pages/posts.ts +130 -0
- package/templates/nestjs/src/app/pages/welcome.ts +151 -0
- package/templates/nestjs/src/collections/posts.collection.ts +89 -0
- package/templates/nestjs/src/generated/.gitkeep +0 -0
- package/templates/nestjs/src/index.html.tmpl +20 -0
- package/templates/nestjs/src/main.server.ts +8 -0
- package/templates/nestjs/src/main.ts +5 -0
- package/templates/nestjs/src/momentum.config.ts.tmpl +73 -0
- package/templates/nestjs/src/server.ts.tmpl +84 -0
- package/templates/nestjs/src/styles.css +140 -0
- package/templates/nestjs/src/types/.gitkeep +0 -0
- package/templates/nestjs/tailwind.config.js +11 -0
- package/templates/nestjs/tsconfig.app.json +9 -0
- package/templates/nestjs/tsconfig.json +26 -0
- package/templates/shared/.claude/agents.md +10 -5
- package/templates/shared/.claude/skills/add-plugin/SKILL.md +38 -1
- package/templates/shared/.claude/skills/admin-config/SKILL.md +76 -0
- package/templates/shared/.claude/skills/api-route/SKILL.md +98 -0
- package/templates/shared/.claude/skills/collection/SKILL.md +2 -2
- package/templates/shared/.claude/skills/component/SKILL.md +136 -0
- package/templates/shared/.claude/skills/e2e-test/SKILL.md +164 -0
- package/templates/shared/.claude/skills/migrations/SKILL.md +55 -0
- package/templates/shared/.claude/skills/momentum-api/SKILL.md +96 -16
- package/templates/shared/docker-compose.yml.tmpl +1 -1
package/index.cjs
CHANGED
|
@@ -34,7 +34,10 @@ var import_fs_extra = __toESM(require("fs-extra"));
|
|
|
34
34
|
var import_picocolors = __toESM(require("picocolors"));
|
|
35
35
|
var TEMPLATE_EXT = ".tmpl";
|
|
36
36
|
function getTemplatesDir() {
|
|
37
|
-
|
|
37
|
+
const distPath = import_node_path.default.resolve(__dirname, "templates");
|
|
38
|
+
if (import_fs_extra.default.existsSync(distPath))
|
|
39
|
+
return distPath;
|
|
40
|
+
return import_node_path.default.resolve(__dirname, "..", "templates");
|
|
38
41
|
}
|
|
39
42
|
function interpolate(content, vars) {
|
|
40
43
|
let result = content;
|
|
@@ -163,7 +166,8 @@ Directory "${projectName}" already exists.`));
|
|
|
163
166
|
process.exit(1);
|
|
164
167
|
}
|
|
165
168
|
const templatesDir = getTemplatesDir();
|
|
166
|
-
const
|
|
169
|
+
const pkgJsonPath = import_fs_extra.default.existsSync(import_node_path.default.resolve(__dirname, "package.json")) ? import_node_path.default.resolve(__dirname, "package.json") : import_node_path.default.resolve(__dirname, "..", "package.json");
|
|
170
|
+
const pkgJson = import_fs_extra.default.readJsonSync(pkgJsonPath);
|
|
167
171
|
const packageVersion = pkgJson.version ?? "0.0.1";
|
|
168
172
|
const vars = {
|
|
169
173
|
projectName,
|
|
@@ -171,9 +175,9 @@ Directory "${projectName}" already exists.`));
|
|
|
171
175
|
databaseType: database,
|
|
172
176
|
dbImport: database === "postgres" ? "import { postgresAdapter } from '@momentumcms/db-drizzle';" : "import { sqliteAdapter } from '@momentumcms/db-drizzle';",
|
|
173
177
|
dbAdapter: database === "postgres" ? `postgresAdapter({
|
|
174
|
-
connectionString: process.env['DATABASE_URL'] ?? 'postgresql://postgres:postgres@localhost:5432
|
|
178
|
+
connectionString: process.env['DATABASE_URL'] ?? 'postgresql://postgres:postgres@localhost:5432/${projectName}',
|
|
175
179
|
})` : `sqliteAdapter({
|
|
176
|
-
filename: process.env['DATABASE_PATH'] ?? './data
|
|
180
|
+
filename: process.env['DATABASE_PATH'] ?? './data/${projectName}.db',
|
|
177
181
|
})`,
|
|
178
182
|
dbPoolSetup: database === "postgres" ? `import type { PostgresAdapterWithRaw } from '@momentumcms/db-drizzle';
|
|
179
183
|
|
|
@@ -182,9 +186,17 @@ const pool = (dbAdapter as PostgresAdapterWithRaw).getPool();` : "",
|
|
|
182
186
|
authDbConfig: database === "postgres" ? "db: { type: 'postgres', pool }," : "db: { type: 'sqlite', database: dbAdapter.getRawDatabase() },",
|
|
183
187
|
dbPackage: database === "postgres" ? '"pg": "^8.18.0"' : '"better-sqlite3": "^12.6.0"',
|
|
184
188
|
dbDevPackage: database === "postgres" ? "" : '"@types/better-sqlite3": "^7.6.13",',
|
|
185
|
-
envDbVar: database === "postgres" ?
|
|
189
|
+
envDbVar: database === "postgres" ? `DATABASE_URL=postgresql://postgres:postgres@localhost:5432/${projectName}` : `DATABASE_PATH=./data/${projectName}.db`,
|
|
186
190
|
defaultPort: "4200",
|
|
187
|
-
externalDependencies:
|
|
191
|
+
externalDependencies: [
|
|
192
|
+
database === "postgres" ? '"pg", "pg-native"' : '"better-sqlite3"',
|
|
193
|
+
...flavor === "nestjs" ? [
|
|
194
|
+
'"@nestjs/microservices"',
|
|
195
|
+
'"@nestjs/websockets"',
|
|
196
|
+
'"class-validator"',
|
|
197
|
+
'"class-transformer"'
|
|
198
|
+
] : []
|
|
199
|
+
].join(", "),
|
|
188
200
|
prerequisitesDocker: database === "postgres" ? `- **Docker** (for PostgreSQL database)
|
|
189
201
|
- [macOS](https://www.docker.com/products/docker-desktop/)
|
|
190
202
|
- [Linux](https://docs.docker.com/engine/install/)
|
|
@@ -196,7 +208,7 @@ This project uses PostgreSQL via Docker. The database is configured in \`docker-
|
|
|
196
208
|
**Connection Details:**
|
|
197
209
|
- Host: \`localhost\`
|
|
198
210
|
- Port: \`5432\`
|
|
199
|
-
- Database: \`
|
|
211
|
+
- Database: \`${projectName}\`
|
|
200
212
|
- Username: \`postgres\`
|
|
201
213
|
- Password: \`postgres\`
|
|
202
214
|
|
|
@@ -218,7 +230,7 @@ docker compose logs -f postgres
|
|
|
218
230
|
|
|
219
231
|
You can also use an external PostgreSQL instance by updating \`DATABASE_URL\` in \`.env\`.` : `### SQLite
|
|
220
232
|
|
|
221
|
-
This project uses SQLite with the database file at \`./data
|
|
233
|
+
This project uses SQLite with the database file at \`./data/${projectName}.db\`.
|
|
222
234
|
The database is automatically created on first run - no setup required.`
|
|
223
235
|
};
|
|
224
236
|
console.log();
|
|
@@ -325,7 +337,7 @@ function parseArgs(argv) {
|
|
|
325
337
|
const arg = args[i];
|
|
326
338
|
if (arg === "--flavor" && args[i + 1]) {
|
|
327
339
|
const val = args[++i];
|
|
328
|
-
if (val === "angular" || val === "analog") {
|
|
340
|
+
if (val === "angular" || val === "analog" || val === "nestjs") {
|
|
329
341
|
opts.flavor = val;
|
|
330
342
|
}
|
|
331
343
|
} else if (arg === "--database" && args[i + 1]) {
|
|
@@ -369,7 +381,8 @@ async function runCLI() {
|
|
|
369
381
|
message: "Which framework?",
|
|
370
382
|
choices: [
|
|
371
383
|
{ title: "Angular (Express SSR)", value: "angular" },
|
|
372
|
-
{ title: "Analog (Nitro)", value: "analog" }
|
|
384
|
+
{ title: "Analog (Nitro)", value: "analog" },
|
|
385
|
+
{ title: "NestJS (Express)", value: "nestjs" }
|
|
373
386
|
]
|
|
374
387
|
},
|
|
375
388
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-momentum-app",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Create a new Momentum CMS application",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Momentum CMS Contributors",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"fs-extra": "^11.2.0",
|
|
58
58
|
"graphql": "16.13.0",
|
|
59
59
|
"h3": "1.15.5",
|
|
60
|
+
"juice": "11.1.1",
|
|
60
61
|
"picocolors": "^1.1.0",
|
|
61
62
|
"prompts": "^2.4.2",
|
|
62
63
|
"rxjs": "7.8.2"
|
|
@@ -11,13 +11,13 @@ const dbAdapter = {{dbAdapter}};
|
|
|
11
11
|
|
|
12
12
|
{{dbPoolSetup}}
|
|
13
13
|
|
|
14
|
-
const
|
|
15
|
-
|
|
14
|
+
const PORT = Number(process.env['PORT']) || {{defaultPort}};
|
|
15
|
+
const BASE_URL = process.env['BETTER_AUTH_URL'] || `http://localhost:${PORT}`;
|
|
16
16
|
|
|
17
17
|
export const authPlugin = momentumAuth({
|
|
18
18
|
{{authDbConfig}}
|
|
19
|
-
baseURL:
|
|
20
|
-
trustedOrigins: [
|
|
19
|
+
baseURL: BASE_URL,
|
|
20
|
+
trustedOrigins: [BASE_URL],
|
|
21
21
|
email: {
|
|
22
22
|
appName: '{{projectName}}',
|
|
23
23
|
},
|
|
@@ -28,7 +28,7 @@ export const authPlugin = momentumAuth({
|
|
|
28
28
|
*/
|
|
29
29
|
export const seo = seoPlugin({
|
|
30
30
|
collections: ['posts'],
|
|
31
|
-
siteUrl:
|
|
31
|
+
siteUrl: BASE_URL,
|
|
32
32
|
analysis: true,
|
|
33
33
|
sitemap: true,
|
|
34
34
|
robots: true,
|
|
@@ -51,7 +51,7 @@ const config = defineMomentumConfig({
|
|
|
51
51
|
},
|
|
52
52
|
},
|
|
53
53
|
server: {
|
|
54
|
-
port:
|
|
54
|
+
port: PORT,
|
|
55
55
|
cors: {
|
|
56
56
|
origin: process.env['CORS_ORIGIN'] || '*',
|
|
57
57
|
methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'OPTIONS'],
|
|
@@ -13,16 +13,16 @@ const dbAdapter = {{dbAdapter}};
|
|
|
13
13
|
|
|
14
14
|
{{dbPoolSetup}}
|
|
15
15
|
|
|
16
|
-
const
|
|
17
|
-
|
|
16
|
+
const PORT = Number(process.env['PORT']) || {{defaultPort}};
|
|
17
|
+
const BASE_URL = process.env['BETTER_AUTH_URL'] || `http://localhost:${PORT}`;
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Auth plugin - manages Better Auth integration, user tables, and middleware.
|
|
21
21
|
*/
|
|
22
22
|
export const authPlugin = momentumAuth({
|
|
23
23
|
{{authDbConfig}}
|
|
24
|
-
baseURL:
|
|
25
|
-
trustedOrigins: [
|
|
24
|
+
baseURL: BASE_URL,
|
|
25
|
+
trustedOrigins: [BASE_URL],
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -30,7 +30,7 @@ export const authPlugin = momentumAuth({
|
|
|
30
30
|
*/
|
|
31
31
|
export const seo = seoPlugin({
|
|
32
32
|
collections: ['posts'],
|
|
33
|
-
siteUrl:
|
|
33
|
+
siteUrl: BASE_URL,
|
|
34
34
|
analysis: true,
|
|
35
35
|
sitemap: true,
|
|
36
36
|
robots: true,
|
|
@@ -56,7 +56,7 @@ const config = defineMomentumConfig({
|
|
|
56
56
|
},
|
|
57
57
|
},
|
|
58
58
|
server: {
|
|
59
|
-
port:
|
|
59
|
+
port: PORT,
|
|
60
60
|
cors: {
|
|
61
61
|
origin: '*',
|
|
62
62
|
methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'OPTIONS'],
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"newProjectRoot": "projects",
|
|
5
|
+
"projects": {
|
|
6
|
+
"{{projectName}}": {
|
|
7
|
+
"projectType": "application",
|
|
8
|
+
"root": "",
|
|
9
|
+
"sourceRoot": "src",
|
|
10
|
+
"prefix": "app",
|
|
11
|
+
"architect": {
|
|
12
|
+
"build": {
|
|
13
|
+
"builder": "@angular/build:application",
|
|
14
|
+
"options": {
|
|
15
|
+
"outputPath": "dist/{{projectName}}",
|
|
16
|
+
"index": "src/index.html",
|
|
17
|
+
"browser": "src/main.ts",
|
|
18
|
+
"server": "src/main.server.ts",
|
|
19
|
+
"tsConfig": "tsconfig.app.json",
|
|
20
|
+
"assets": [
|
|
21
|
+
{
|
|
22
|
+
"glob": "**/*",
|
|
23
|
+
"input": "public"
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"styles": ["src/styles.css"],
|
|
27
|
+
"scripts": [],
|
|
28
|
+
"ssr": {
|
|
29
|
+
"entry": "src/server.ts"
|
|
30
|
+
},
|
|
31
|
+
"outputMode": "server",
|
|
32
|
+
"externalDependencies": [{{externalDependencies}}]
|
|
33
|
+
},
|
|
34
|
+
"configurations": {
|
|
35
|
+
"production": {
|
|
36
|
+
"budgets": [
|
|
37
|
+
{
|
|
38
|
+
"type": "initial",
|
|
39
|
+
"maximumWarning": "1MB",
|
|
40
|
+
"maximumError": "2MB"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"type": "anyComponentStyle",
|
|
44
|
+
"maximumWarning": "4kB",
|
|
45
|
+
"maximumError": "8kB"
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"outputHashing": "all"
|
|
49
|
+
},
|
|
50
|
+
"development": {
|
|
51
|
+
"optimization": false,
|
|
52
|
+
"extractLicenses": false,
|
|
53
|
+
"sourceMap": true
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"defaultConfiguration": "production"
|
|
57
|
+
},
|
|
58
|
+
"serve": {
|
|
59
|
+
"builder": "@angular/build:dev-server",
|
|
60
|
+
"configurations": {
|
|
61
|
+
"production": {
|
|
62
|
+
"buildTarget": "{{projectName}}:build:production"
|
|
63
|
+
},
|
|
64
|
+
"development": {
|
|
65
|
+
"buildTarget": "{{projectName}}:build:development"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"defaultConfiguration": "development"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import tseslint from 'typescript-eslint';
|
|
2
|
+
import angular from 'angular-eslint';
|
|
3
|
+
|
|
4
|
+
export default tseslint.config(
|
|
5
|
+
{
|
|
6
|
+
files: ['**/*.ts'],
|
|
7
|
+
extends: [...tseslint.configs.recommended, ...angular.configs.tsRecommended],
|
|
8
|
+
rules: {
|
|
9
|
+
'@angular-eslint/directive-selector': [
|
|
10
|
+
'error',
|
|
11
|
+
{
|
|
12
|
+
type: 'attribute',
|
|
13
|
+
prefix: 'app',
|
|
14
|
+
style: 'camelCase',
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
'@angular-eslint/component-selector': [
|
|
18
|
+
'error',
|
|
19
|
+
{
|
|
20
|
+
type: 'element',
|
|
21
|
+
prefix: 'app',
|
|
22
|
+
style: 'kebab-case',
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
files: ['**/*.html'],
|
|
29
|
+
extends: [...angular.configs.templateRecommended, ...angular.configs.templateAccessibility],
|
|
30
|
+
},
|
|
31
|
+
);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "ng serve",
|
|
7
|
+
"build": "ng build",
|
|
8
|
+
"start": "node dist/{{projectName}}/server/server.mjs",
|
|
9
|
+
"lint": "ng lint",
|
|
10
|
+
"generate": "tsx node_modules/@momentumcms/core/src/generators/generator.cjs src/momentum.config.ts --types src/generated/momentum.types.ts --config src/generated/momentum.config.ts",
|
|
11
|
+
"migrate:generate": "ng generate @momentumcms/migrations:generate",
|
|
12
|
+
"migrate:run": "ng generate @momentumcms/migrations:run",
|
|
13
|
+
"migrate:status": "ng generate @momentumcms/migrations:status",
|
|
14
|
+
"migrate:rollback": "ng generate @momentumcms/migrations:rollback"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@angular/animations": "^21.1.0",
|
|
18
|
+
"@angular/aria": "^21.1.0",
|
|
19
|
+
"@angular/cdk": "^21.1.0",
|
|
20
|
+
"@angular/common": "^21.1.0",
|
|
21
|
+
"@angular/compiler": "^21.1.0",
|
|
22
|
+
"@angular/core": "^21.1.0",
|
|
23
|
+
"@angular/forms": "^21.1.0",
|
|
24
|
+
"@angular/platform-browser": "^21.1.0",
|
|
25
|
+
"@angular/platform-server": "^21.1.0",
|
|
26
|
+
"@angular/router": "^21.1.0",
|
|
27
|
+
"@angular/ssr": "^21.1.0",
|
|
28
|
+
"@momentumcms/admin": "^{{packageVersion}}",
|
|
29
|
+
"@momentumcms/auth": "^{{packageVersion}}",
|
|
30
|
+
"@momentumcms/core": "^{{packageVersion}}",
|
|
31
|
+
"@momentumcms/db-drizzle": "^{{packageVersion}}",
|
|
32
|
+
"@momentumcms/server-core": "^{{packageVersion}}",
|
|
33
|
+
"@momentumcms/server-nestjs": "^{{packageVersion}}",
|
|
34
|
+
"@momentumcms/storage": "^{{packageVersion}}",
|
|
35
|
+
"@momentumcms/logger": "^{{packageVersion}}",
|
|
36
|
+
"@momentumcms/migrations": "^{{packageVersion}}",
|
|
37
|
+
"@momentumcms/plugins-core": "^{{packageVersion}}",
|
|
38
|
+
"@momentumcms/plugins-seo": "^{{packageVersion}}",
|
|
39
|
+
"@nestjs/common": "^11.1.0",
|
|
40
|
+
"@nestjs/core": "^11.1.0",
|
|
41
|
+
"@nestjs/platform-express": "^11.1.0",
|
|
42
|
+
"@ng-icons/core": "^33.0.0",
|
|
43
|
+
"@ng-icons/heroicons": "^33.0.0",
|
|
44
|
+
"@tiptap/core": "^3.0.0",
|
|
45
|
+
"@tiptap/extension-link": "^3.0.0",
|
|
46
|
+
"@tiptap/extension-placeholder": "^3.0.0",
|
|
47
|
+
"@tiptap/extension-underline": "^3.0.0",
|
|
48
|
+
"@tiptap/starter-kit": "^3.0.0",
|
|
49
|
+
"dotenv": "^16.5.0",
|
|
50
|
+
"express": "^4.21.0",
|
|
51
|
+
{{dbPackage}},
|
|
52
|
+
"reflect-metadata": "^0.2.0",
|
|
53
|
+
"rxjs": "^7.8.0",
|
|
54
|
+
"tslib": "^2.8.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@angular/build": "^21.1.0",
|
|
58
|
+
"@angular/cli": "^21.1.0",
|
|
59
|
+
"@angular/compiler-cli": "^21.1.0",
|
|
60
|
+
{{dbDevPackage}}
|
|
61
|
+
"@types/express": "^4.17.0",
|
|
62
|
+
"@types/node": "^22.15.0",
|
|
63
|
+
"autoprefixer": "^10.4.21",
|
|
64
|
+
"postcss": "^8.5.4",
|
|
65
|
+
"tailwindcss": "^3.4.17",
|
|
66
|
+
"tsx": "^4.0.0",
|
|
67
|
+
"typescript": "~5.9.2"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
|
|
2
|
+
import { provideServerRendering, withRoutes } from '@angular/ssr';
|
|
3
|
+
import { appConfig } from './app.config';
|
|
4
|
+
import { serverRoutes } from './app.routes.server';
|
|
5
|
+
|
|
6
|
+
const serverConfig: ApplicationConfig = {
|
|
7
|
+
providers: [provideServerRendering(withRoutes(serverRoutes))],
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const config = mergeApplicationConfig(appConfig, serverConfig);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
|
|
2
|
+
import { provideRouter, withViewTransitions } from '@angular/router';
|
|
3
|
+
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
|
|
4
|
+
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
|
|
5
|
+
import { crudToastInterceptor, provideMomentumFieldRenderers } from '@momentumcms/admin';
|
|
6
|
+
import { routes } from './app.routes';
|
|
7
|
+
import { providePostBlocks } from './pages/post-block-providers';
|
|
8
|
+
|
|
9
|
+
export const appConfig: ApplicationConfig = {
|
|
10
|
+
providers: [
|
|
11
|
+
provideHttpClient(withFetch(), withInterceptors([crudToastInterceptor])),
|
|
12
|
+
provideClientHydration(withEventReplay()),
|
|
13
|
+
provideBrowserGlobalErrorListeners(),
|
|
14
|
+
provideRouter(routes, withViewTransitions()),
|
|
15
|
+
provideMomentumFieldRenderers(),
|
|
16
|
+
...providePostBlocks(),
|
|
17
|
+
],
|
|
18
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { RenderMode, ServerRoute } from '@angular/ssr';
|
|
2
|
+
|
|
3
|
+
export const serverRoutes: ServerRoute[] = [
|
|
4
|
+
// Admin routes render client-side — guards need to run without SSR hydration conflict
|
|
5
|
+
{ path: 'admin/**', renderMode: RenderMode.Client },
|
|
6
|
+
// All other routes use server-side rendering
|
|
7
|
+
{ path: '**', renderMode: RenderMode.Server },
|
|
8
|
+
];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Route } from '@angular/router';
|
|
2
|
+
import { momentumAdminRoutes } from '@momentumcms/admin';
|
|
3
|
+
import { adminConfig } from '../generated/momentum.config';
|
|
4
|
+
|
|
5
|
+
export const routes: Route[] = [
|
|
6
|
+
{
|
|
7
|
+
path: '',
|
|
8
|
+
loadComponent: () => import('./pages/welcome').then((m) => m.WelcomePage),
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
path: 'posts',
|
|
12
|
+
loadComponent: () => import('./pages/posts').then((m) => m.PostsPageComponent),
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
path: 'posts/:slug',
|
|
16
|
+
loadComponent: () => import('./pages/post-detail').then((m) => m.PostDetailComponent),
|
|
17
|
+
resolve: {
|
|
18
|
+
postData: () => import('./pages/post-detail.resolver').then((m) => m.postDetailResolver),
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
...momentumAdminRoutes(adminConfig),
|
|
22
|
+
];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Component({
|
|
4
|
+
selector: 'app-hero-block',
|
|
5
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
6
|
+
host: {
|
|
7
|
+
class: 'block',
|
|
8
|
+
'[attr.data-testid]': '"block-hero"',
|
|
9
|
+
},
|
|
10
|
+
template: `
|
|
11
|
+
<section class="bg-primary text-primary-foreground py-12 px-4 md:py-20 md:px-8 text-center">
|
|
12
|
+
<div class="mx-auto max-w-4xl">
|
|
13
|
+
<h1 class="text-3xl md:text-4xl lg:text-5xl font-bold mb-4" data-testid="hero-heading">
|
|
14
|
+
{{ heading() }}
|
|
15
|
+
</h1>
|
|
16
|
+
@if (subheading()) {
|
|
17
|
+
<p class="text-lg md:text-xl text-primary-foreground mb-8" data-testid="hero-subheading">
|
|
18
|
+
{{ subheading() }}
|
|
19
|
+
</p>
|
|
20
|
+
}
|
|
21
|
+
@if (ctaText()) {
|
|
22
|
+
<a
|
|
23
|
+
class="inline-block bg-primary-foreground text-primary font-semibold px-6 py-3 rounded-lg hover:bg-primary-foreground/90 transition-colors"
|
|
24
|
+
[href]="ctaLink() || '#'"
|
|
25
|
+
data-testid="hero-cta"
|
|
26
|
+
>
|
|
27
|
+
{{ ctaText() }}
|
|
28
|
+
</a>
|
|
29
|
+
}
|
|
30
|
+
</div>
|
|
31
|
+
</section>
|
|
32
|
+
`,
|
|
33
|
+
})
|
|
34
|
+
export class HeroBlockComponent {
|
|
35
|
+
readonly data = input.required<Record<string, unknown>>();
|
|
36
|
+
|
|
37
|
+
readonly heading = computed((): string => String(this.data()['heading'] ?? ''));
|
|
38
|
+
readonly subheading = computed((): string => String(this.data()['subheading'] ?? ''));
|
|
39
|
+
readonly ctaText = computed((): string => String(this.data()['ctaText'] ?? ''));
|
|
40
|
+
readonly ctaLink = computed((): string => String(this.data()['ctaLink'] ?? ''));
|
|
41
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Component({
|
|
4
|
+
selector: 'app-image-text-block',
|
|
5
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
6
|
+
host: {
|
|
7
|
+
class: 'block',
|
|
8
|
+
'[attr.data-testid]': '"block-imageText"',
|
|
9
|
+
},
|
|
10
|
+
template: `
|
|
11
|
+
<section class="py-8 px-4 md:py-16 md:px-8">
|
|
12
|
+
<div
|
|
13
|
+
class="mx-auto max-w-6xl flex flex-col gap-8 md:gap-12 items-center"
|
|
14
|
+
[class.md:flex-row]="!reversed()"
|
|
15
|
+
[class.md:flex-row-reverse]="reversed()"
|
|
16
|
+
>
|
|
17
|
+
<!-- Image -->
|
|
18
|
+
<div class="w-full md:w-1/2" data-testid="image-text-image">
|
|
19
|
+
@if (imageUrl()) {
|
|
20
|
+
<img
|
|
21
|
+
[src]="imageUrl()"
|
|
22
|
+
[alt]="imageAlt() || heading()"
|
|
23
|
+
class="w-full h-auto rounded-lg object-cover bg-muted"
|
|
24
|
+
/>
|
|
25
|
+
} @else {
|
|
26
|
+
<div class="w-full aspect-video rounded-lg bg-muted flex items-center justify-center">
|
|
27
|
+
<span class="text-muted-foreground text-sm">No image</span>
|
|
28
|
+
</div>
|
|
29
|
+
}
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- Text -->
|
|
33
|
+
<div class="w-full md:w-1/2">
|
|
34
|
+
<h2
|
|
35
|
+
class="text-2xl md:text-3xl font-bold text-foreground mb-4"
|
|
36
|
+
data-testid="image-text-heading"
|
|
37
|
+
>
|
|
38
|
+
{{ heading() }}
|
|
39
|
+
</h2>
|
|
40
|
+
<p class="text-lg text-muted-foreground leading-relaxed" data-testid="image-text-body">
|
|
41
|
+
{{ body() }}
|
|
42
|
+
</p>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</section>
|
|
46
|
+
`,
|
|
47
|
+
})
|
|
48
|
+
export class ImageTextBlockComponent {
|
|
49
|
+
readonly data = input.required<Record<string, unknown>>();
|
|
50
|
+
|
|
51
|
+
readonly heading = computed((): string => String(this.data()['heading'] ?? ''));
|
|
52
|
+
readonly body = computed((): string => String(this.data()['body'] ?? ''));
|
|
53
|
+
readonly imageUrl = computed((): string => String(this.data()['imageUrl'] ?? ''));
|
|
54
|
+
readonly imageAlt = computed((): string => String(this.data()['imageAlt'] ?? ''));
|
|
55
|
+
readonly reversed = computed((): boolean => this.data()['imagePosition'] === 'right');
|
|
56
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Component({
|
|
4
|
+
selector: 'app-text-block',
|
|
5
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
6
|
+
host: {
|
|
7
|
+
class: 'block',
|
|
8
|
+
'[attr.data-testid]': '"block-textBlock"',
|
|
9
|
+
},
|
|
10
|
+
template: `
|
|
11
|
+
<section class="py-8 px-4 md:py-12 md:px-8 max-w-3xl mx-auto">
|
|
12
|
+
@if (heading()) {
|
|
13
|
+
<h2 class="text-2xl font-bold text-foreground mb-4" data-testid="text-heading">
|
|
14
|
+
{{ heading() }}
|
|
15
|
+
</h2>
|
|
16
|
+
}
|
|
17
|
+
<p class="text-lg text-muted-foreground leading-relaxed" data-testid="text-body">
|
|
18
|
+
{{ body() }}
|
|
19
|
+
</p>
|
|
20
|
+
</section>
|
|
21
|
+
`,
|
|
22
|
+
})
|
|
23
|
+
export class TextBlockComponent {
|
|
24
|
+
readonly data = input.required<Record<string, unknown>>();
|
|
25
|
+
|
|
26
|
+
readonly heading = computed((): string => String(this.data()['heading'] ?? ''));
|
|
27
|
+
readonly body = computed((): string => String(this.data()['body'] ?? ''));
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Provider } from '@angular/core';
|
|
2
|
+
import { provideBlockComponents } from '@momentumcms/ui';
|
|
3
|
+
import { HeroBlockComponent } from './blocks/hero-block.component';
|
|
4
|
+
import { TextBlockComponent } from './blocks/text-block.component';
|
|
5
|
+
import { ImageTextBlockComponent } from './blocks/image-text-block.component';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Provide all post block components for the block renderer.
|
|
9
|
+
* Add to the app's root providers.
|
|
10
|
+
*/
|
|
11
|
+
export function providePostBlocks(): Provider[] {
|
|
12
|
+
return provideBlockComponents({
|
|
13
|
+
hero: HeroBlockComponent,
|
|
14
|
+
textBlock: TextBlockComponent,
|
|
15
|
+
imageText: ImageTextBlockComponent,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ResolveFn } from '@angular/router';
|
|
2
|
+
import { injectMomentumAPI, type FindResult } from '@momentumcms/admin';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolver for the post detail route.
|
|
6
|
+
*
|
|
7
|
+
* Fetches the post by slug before the component renders. This ensures:
|
|
8
|
+
* - SSR renders the full post content (not just "Loading...")
|
|
9
|
+
* - The admin preview iframe (with scripts disabled) shows the real post
|
|
10
|
+
*/
|
|
11
|
+
export const postDetailResolver: ResolveFn<FindResult<Record<string, unknown>>> = (route) => {
|
|
12
|
+
const api = injectMomentumAPI();
|
|
13
|
+
const slug = typeof route.params['slug'] === 'string' ? route.params['slug'] : undefined;
|
|
14
|
+
|
|
15
|
+
if (!slug) {
|
|
16
|
+
return {
|
|
17
|
+
docs: [],
|
|
18
|
+
totalDocs: 0,
|
|
19
|
+
page: 1,
|
|
20
|
+
totalPages: 0,
|
|
21
|
+
limit: 1,
|
|
22
|
+
hasNextPage: false,
|
|
23
|
+
hasPrevPage: false,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return api.collection('posts').find({
|
|
28
|
+
where: { slug: { equals: slug } },
|
|
29
|
+
limit: 1,
|
|
30
|
+
});
|
|
31
|
+
};
|