create-momentum-app 0.5.0 → 0.5.2
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/README.md +1 -0
- package/index.cjs +12 -3
- package/package.json +22 -21
- package/templates/angular/src/server.ts.tmpl +3 -1
- 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/README.md
CHANGED
|
@@ -30,4 +30,5 @@ npx create-momentum-app my-app --flavor angular --database postgres
|
|
|
30
30
|
- REST API at `/api`
|
|
31
31
|
- Authentication via Better Auth
|
|
32
32
|
- Drizzle ORM with PostgreSQL or SQLite
|
|
33
|
+
- Docker Compose setup for PostgreSQL (when using postgres)
|
|
33
34
|
- Tailwind CSS with the Momentum admin theme
|
package/index.cjs
CHANGED
|
@@ -184,7 +184,15 @@ const pool = (dbAdapter as PostgresAdapterWithRaw).getPool();` : "",
|
|
|
184
184
|
dbDevPackage: database === "postgres" ? "" : '"@types/better-sqlite3": "^7.6.13",',
|
|
185
185
|
envDbVar: database === "postgres" ? "DATABASE_URL=postgresql://postgres:postgres@localhost:5432/momentum" : "DATABASE_PATH=./data/momentum.db",
|
|
186
186
|
defaultPort: "4200",
|
|
187
|
-
externalDependencies:
|
|
187
|
+
externalDependencies: [
|
|
188
|
+
database === "postgres" ? '"pg", "pg-native"' : '"better-sqlite3"',
|
|
189
|
+
...flavor === "nestjs" ? [
|
|
190
|
+
'"@nestjs/microservices"',
|
|
191
|
+
'"@nestjs/websockets"',
|
|
192
|
+
'"class-validator"',
|
|
193
|
+
'"class-transformer"'
|
|
194
|
+
] : []
|
|
195
|
+
].join(", "),
|
|
188
196
|
prerequisitesDocker: database === "postgres" ? `- **Docker** (for PostgreSQL database)
|
|
189
197
|
- [macOS](https://www.docker.com/products/docker-desktop/)
|
|
190
198
|
- [Linux](https://docs.docker.com/engine/install/)
|
|
@@ -325,7 +333,7 @@ function parseArgs(argv) {
|
|
|
325
333
|
const arg = args[i];
|
|
326
334
|
if (arg === "--flavor" && args[i + 1]) {
|
|
327
335
|
const val = args[++i];
|
|
328
|
-
if (val === "angular" || val === "analog") {
|
|
336
|
+
if (val === "angular" || val === "analog" || val === "nestjs") {
|
|
329
337
|
opts.flavor = val;
|
|
330
338
|
}
|
|
331
339
|
} else if (arg === "--database" && args[i + 1]) {
|
|
@@ -369,7 +377,8 @@ async function runCLI() {
|
|
|
369
377
|
message: "Which framework?",
|
|
370
378
|
choices: [
|
|
371
379
|
{ title: "Angular (Express SSR)", value: "angular" },
|
|
372
|
-
{ title: "Analog (Nitro)", value: "analog" }
|
|
380
|
+
{ title: "Analog (Nitro)", value: "analog" },
|
|
381
|
+
{ title: "NestJS (Express)", value: "nestjs" }
|
|
373
382
|
]
|
|
374
383
|
},
|
|
375
384
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-momentum-app",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Create a new Momentum CMS application",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Momentum CMS Contributors",
|
|
@@ -35,28 +35,29 @@
|
|
|
35
35
|
"README.md"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@angular/aria": "21.
|
|
39
|
-
"@angular/cdk": "21.
|
|
40
|
-
"@angular/common": "21.
|
|
41
|
-
"@angular/compiler": "21.
|
|
42
|
-
"@angular/core": "21.
|
|
43
|
-
"@angular/forms": "21.
|
|
44
|
-
"@angular/platform-browser": "21.
|
|
45
|
-
"@angular/platform-server": "21.
|
|
46
|
-
"@angular/router": "21.
|
|
47
|
-
"@angular/ssr": "21.
|
|
48
|
-
"@aws-sdk/client-s3": "3.
|
|
49
|
-
"@aws-sdk/s3-request-presigner": "3.
|
|
50
|
-
"@ng-icons/core": "33.
|
|
51
|
-
"@ng-icons/heroicons": "33.
|
|
52
|
-
"@tiptap/core": "3.
|
|
53
|
-
"@tiptap/extension-link": "3.
|
|
54
|
-
"@tiptap/extension-placeholder": "3.
|
|
55
|
-
"@tiptap/extension-underline": "3.
|
|
56
|
-
"@tiptap/starter-kit": "3.
|
|
38
|
+
"@angular/aria": "21.2.0",
|
|
39
|
+
"@angular/cdk": "21.2.0",
|
|
40
|
+
"@angular/common": "21.2.0",
|
|
41
|
+
"@angular/compiler": "21.2.0",
|
|
42
|
+
"@angular/core": "21.2.0",
|
|
43
|
+
"@angular/forms": "21.2.0",
|
|
44
|
+
"@angular/platform-browser": "21.2.0",
|
|
45
|
+
"@angular/platform-server": "21.2.0",
|
|
46
|
+
"@angular/router": "21.2.0",
|
|
47
|
+
"@angular/ssr": "21.2.0",
|
|
48
|
+
"@aws-sdk/client-s3": "3.999.0",
|
|
49
|
+
"@aws-sdk/s3-request-presigner": "3.999.0",
|
|
50
|
+
"@ng-icons/core": "33.1.0",
|
|
51
|
+
"@ng-icons/heroicons": "33.1.0",
|
|
52
|
+
"@tiptap/core": "3.20.0",
|
|
53
|
+
"@tiptap/extension-link": "3.20.0",
|
|
54
|
+
"@tiptap/extension-placeholder": "3.20.0",
|
|
55
|
+
"@tiptap/extension-underline": "3.20.0",
|
|
56
|
+
"@tiptap/starter-kit": "3.20.0",
|
|
57
57
|
"fs-extra": "^11.2.0",
|
|
58
|
-
"graphql": "16.
|
|
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"
|
|
@@ -16,7 +16,9 @@ import momentumConfig, { authPlugin } from './momentum.config';
|
|
|
16
16
|
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
const browserDistFolder = resolve(serverDistFolder, '../browser');
|
|
18
18
|
|
|
19
|
-
const angularApp = new AngularNodeAppEngine(
|
|
19
|
+
const angularApp = new AngularNodeAppEngine({
|
|
20
|
+
allowedHosts: ['localhost'],
|
|
21
|
+
});
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* Create the Momentum CMS server.
|
|
@@ -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
|
+
};
|