@foliokit/cms-core 1.0.0 → 1.0.1

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 (95) hide show
  1. package/README.md +62 -2
  2. package/eslint.config.mjs +48 -0
  3. package/ng-package.json +7 -0
  4. package/package.json +5 -18
  5. package/project.json +32 -0
  6. package/{esm2022/index.js → src/index.ts} +9 -3
  7. package/src/lib/cms-core/cms-core.html +1 -0
  8. package/src/lib/cms-core/cms-core.scss +0 -0
  9. package/src/lib/cms-core/cms-core.spec.ts +44 -0
  10. package/src/lib/cms-core/cms-core.ts +9 -0
  11. package/src/lib/firebase/firebase-admin.ts +32 -0
  12. package/src/lib/firebase/firebase.config.ts +26 -0
  13. package/src/lib/firebase/firebase.providers.ts +89 -0
  14. package/src/lib/firebase/foliokit.providers.ts +178 -0
  15. package/src/lib/models/author.model.ts +16 -0
  16. package/src/lib/models/page.model.ts +11 -0
  17. package/src/lib/models/post.model.ts +41 -0
  18. package/src/lib/models/site-config.model.ts +103 -0
  19. package/src/lib/models/tag.model.ts +5 -0
  20. package/src/lib/pipes/tag-label.pipe.ts +16 -0
  21. package/src/lib/resolvers/about-page.resolver.ts +76 -0
  22. package/src/lib/resolvers/links-page.resolver.ts +77 -0
  23. package/src/lib/resolvers/posts.resolver.ts +51 -0
  24. package/src/lib/services/auth.service.ts +49 -0
  25. package/src/lib/services/author.service.ts +88 -0
  26. package/src/lib/services/post.service.spec.ts +255 -0
  27. package/src/lib/services/post.service.ts +148 -0
  28. package/src/lib/services/site-config.service.ts +86 -0
  29. package/src/lib/services/tag.service.ts +24 -0
  30. package/src/lib/tokens/post-service.token.ts +14 -0
  31. package/src/lib/tokens/site-config-service.token.ts +12 -0
  32. package/src/lib/utils/normalize-author.ts +50 -0
  33. package/src/lib/utils/normalize-post.ts +66 -0
  34. package/src/lib/utils/normalize-site-config.ts +145 -0
  35. package/testing/firestore.stub.ts +65 -0
  36. package/tsconfig.json +31 -0
  37. package/tsconfig.lib.json +12 -0
  38. package/tsconfig.lib.prod.json +9 -0
  39. package/tsconfig.spec.json +8 -0
  40. package/esm2022/foliokit-cms-core.js +0 -5
  41. package/esm2022/foliokit-cms-core.js.map +0 -1
  42. package/esm2022/index.js.map +0 -1
  43. package/esm2022/lib/firebase/firebase.config.js +0 -8
  44. package/esm2022/lib/firebase/firebase.config.js.map +0 -1
  45. package/esm2022/lib/firebase/firebase.providers.js +0 -54
  46. package/esm2022/lib/firebase/firebase.providers.js.map +0 -1
  47. package/esm2022/lib/models/author.model.js +0 -1
  48. package/esm2022/lib/models/author.model.js.map +0 -1
  49. package/esm2022/lib/models/page.model.js +0 -1
  50. package/esm2022/lib/models/page.model.js.map +0 -1
  51. package/esm2022/lib/models/post.model.js +0 -1
  52. package/esm2022/lib/models/post.model.js.map +0 -1
  53. package/esm2022/lib/models/site-config.model.js +0 -1
  54. package/esm2022/lib/models/site-config.model.js.map +0 -1
  55. package/esm2022/lib/models/tag.model.js +0 -1
  56. package/esm2022/lib/models/tag.model.js.map +0 -1
  57. package/esm2022/lib/services/auth.service.js +0 -42
  58. package/esm2022/lib/services/auth.service.js.map +0 -1
  59. package/esm2022/lib/services/page.service.js +0 -73
  60. package/esm2022/lib/services/page.service.js.map +0 -1
  61. package/esm2022/lib/services/post.service.js +0 -83
  62. package/esm2022/lib/services/post.service.js.map +0 -1
  63. package/esm2022/lib/services/site-config.service.js +0 -31
  64. package/esm2022/lib/services/site-config.service.js.map +0 -1
  65. package/esm2022/lib/services/tag.service.js +0 -22
  66. package/esm2022/lib/services/tag.service.js.map +0 -1
  67. package/esm2022/lib/tokens/page-service.token.js +0 -4
  68. package/esm2022/lib/tokens/page-service.token.js.map +0 -1
  69. package/esm2022/lib/tokens/post-service.token.js +0 -5
  70. package/esm2022/lib/tokens/post-service.token.js.map +0 -1
  71. package/esm2022/lib/utils/normalize-page.js +0 -74
  72. package/esm2022/lib/utils/normalize-page.js.map +0 -1
  73. package/esm2022/lib/utils/normalize-post.js +0 -66
  74. package/esm2022/lib/utils/normalize-post.js.map +0 -1
  75. package/esm2022/lib/utils/normalize-site-config.js +0 -62
  76. package/esm2022/lib/utils/normalize-site-config.js.map +0 -1
  77. package/foliokit-cms-core.d.ts +0 -5
  78. package/index.d.ts +0 -14
  79. package/lib/firebase/firebase.config.d.ts +0 -11
  80. package/lib/firebase/firebase.providers.d.ts +0 -3
  81. package/lib/models/author.model.d.ts +0 -14
  82. package/lib/models/page.model.d.ts +0 -40
  83. package/lib/models/post.model.d.ts +0 -39
  84. package/lib/models/site-config.model.d.ts +0 -27
  85. package/lib/models/tag.model.d.ts +0 -5
  86. package/lib/services/auth.service.d.ts +0 -13
  87. package/lib/services/page.service.d.ts +0 -15
  88. package/lib/services/post.service.d.ts +0 -17
  89. package/lib/services/site-config.service.d.ts +0 -10
  90. package/lib/services/tag.service.d.ts +0 -9
  91. package/lib/tokens/page-service.token.d.ts +0 -9
  92. package/lib/tokens/post-service.token.d.ts +0 -10
  93. package/lib/utils/normalize-page.d.ts +0 -2
  94. package/lib/utils/normalize-post.d.ts +0 -6
  95. package/lib/utils/normalize-site-config.d.ts +0 -2
@@ -0,0 +1,145 @@
1
+ import type { SiteConfig, NavItem, SocialLink, SocialPlatform, SeoMeta, AboutPageConfig, LinksPageConfig, HomePageConfig } from '../models/site-config.model';
2
+ import type { LinksLink } from '../models/page.model';
3
+
4
+ function normalizeTimestamp(value: unknown): number {
5
+ if (value == null) return 0;
6
+ if (typeof value === 'number') return value;
7
+ if (value instanceof Date) return value.getTime();
8
+ if (typeof value === 'object') {
9
+ const v = value as Record<string, unknown>;
10
+ if (typeof (v as { toMillis?: unknown }).toMillis === 'function') {
11
+ return (v as { toMillis(): number }).toMillis();
12
+ }
13
+ if (typeof (v as { toDate?: unknown }).toDate === 'function') {
14
+ return (v as { toDate(): Date }).toDate().getTime();
15
+ }
16
+ if (typeof v['_seconds'] === 'number') {
17
+ return (v['_seconds'] as number) * 1000 +
18
+ Math.floor(((v['_nanoseconds'] as number) ?? 0) / 1e6);
19
+ }
20
+ if (typeof v['seconds'] === 'number') {
21
+ return (v['seconds'] as number) * 1000 +
22
+ Math.floor(((v['nanoseconds'] as number) ?? 0) / 1e6);
23
+ }
24
+ }
25
+ return 0;
26
+ }
27
+
28
+ function normalizeNavItems(raw: unknown): NavItem[] {
29
+ if (!Array.isArray(raw)) return [];
30
+ return (raw as Record<string, unknown>[]).map((item) => ({
31
+ label: (item['label'] as string) ?? '',
32
+ url: (item['url'] as string) ?? '',
33
+ order: item['order'] as number | undefined,
34
+ external: item['external'] as boolean | undefined,
35
+ icon: item['icon'] as string | undefined,
36
+ }));
37
+ }
38
+
39
+ function normalizeSocialLinks(raw: unknown): SocialLink[] {
40
+ if (!Array.isArray(raw)) return [];
41
+ return (raw as Record<string, unknown>[]).map((item) => ({
42
+ platform: item['platform'] as SocialPlatform,
43
+ url: (item['url'] as string) ?? '',
44
+ label: item['label'] as string | undefined,
45
+ icon: item['icon'] as string | undefined,
46
+ }));
47
+ }
48
+
49
+ function normalizeSeoMeta(raw: unknown): SeoMeta | undefined {
50
+ if (!raw || typeof raw !== 'object') return undefined;
51
+ const r = raw as Record<string, unknown>;
52
+ return {
53
+ title: r['title'] as string | undefined,
54
+ description: r['description'] as string | undefined,
55
+ keywords: Array.isArray(r['keywords']) ? (r['keywords'] as string[]) : undefined,
56
+ ogImage: r['ogImage'] as string | undefined,
57
+ canonicalUrl: r['canonicalUrl'] as string | undefined,
58
+ noIndex: r['noIndex'] as boolean | undefined,
59
+ };
60
+ }
61
+
62
+ function normalizeLinksLinks(raw: unknown): LinksLink[] {
63
+ if (!Array.isArray(raw)) return [];
64
+ return (raw as Record<string, unknown>[]).map((item) => ({
65
+ id: (item['id'] as string) ?? '',
66
+ label: (item['label'] as string) ?? '',
67
+ url: (item['url'] as string) ?? '',
68
+ icon: item['icon'] as string | undefined,
69
+ platform: item['platform'] as LinksLink['platform'] | undefined,
70
+ highlighted: item['highlighted'] as boolean | undefined,
71
+ order: (item['order'] as number) ?? 0,
72
+ }));
73
+ }
74
+
75
+ function normalizeHomePageConfig(raw: unknown): HomePageConfig | undefined {
76
+ if (!raw || typeof raw !== 'object') return undefined;
77
+ const r = raw as Record<string, unknown>;
78
+ return {
79
+ enabled: (r['enabled'] as boolean) ?? false,
80
+ heroHeadline: (r['heroHeadline'] as string) ?? '',
81
+ heroSubheadline: r['heroSubheadline'] as string | undefined,
82
+ ctaLabel: r['ctaLabel'] as string | undefined,
83
+ ctaUrl: r['ctaUrl'] as string | undefined,
84
+ showRecentPosts: r['showRecentPosts'] as boolean | undefined,
85
+ seo: normalizeSeoMeta(r['seo']),
86
+ };
87
+ }
88
+
89
+ function normalizeAboutPageConfig(raw: unknown): AboutPageConfig {
90
+ const r = (raw && typeof raw === 'object') ? raw as Record<string, unknown> : {};
91
+ return {
92
+ enabled: (r['enabled'] as boolean) ?? false,
93
+ headline: (r['headline'] as string) ?? '',
94
+ subheadline: r['subheadline'] as string | undefined,
95
+ bio: (r['bio'] as string) ?? '',
96
+ photoUrl: r['photoUrl'] as string | undefined,
97
+ photoUrlDark: r['photoUrlDark'] as string | undefined,
98
+ photoAlt: r['photoAlt'] as string | undefined,
99
+ socialLinks: Array.isArray(r['socialLinks'])
100
+ ? normalizeSocialLinks(r['socialLinks'])
101
+ : undefined,
102
+ seo: normalizeSeoMeta(r['seo']),
103
+ };
104
+ }
105
+
106
+ function normalizeLinksPageConfig(raw: unknown): LinksPageConfig {
107
+ const r = (raw && typeof raw === 'object') ? raw as Record<string, unknown> : {};
108
+ return {
109
+ enabled: (r['enabled'] as boolean) ?? false,
110
+ links: normalizeLinksLinks(r['links']),
111
+ title: r['title'] as string | undefined,
112
+ avatarUrl: r['avatarUrl'] as string | undefined,
113
+ avatarUrlDark: r['avatarUrlDark'] as string | undefined,
114
+ avatarAlt: r['avatarAlt'] as string | undefined,
115
+ headline: r['headline'] as string | undefined,
116
+ bio: r['bio'] as string | undefined,
117
+ seo: normalizeSeoMeta(r['seo']),
118
+ };
119
+ }
120
+
121
+ export function normalizeSiteConfig(raw: Record<string, unknown>): SiteConfig {
122
+ const pages = raw['pages'] as Record<string, unknown> | undefined;
123
+
124
+ return {
125
+ id: (raw['id'] as string) ?? '',
126
+ siteName: (raw['siteName'] as string) ?? '',
127
+ siteUrl: (raw['siteUrl'] as string) ?? '',
128
+ description: raw['description'] as string | undefined,
129
+ logo: raw['logo'] as string | undefined,
130
+ favicon: raw['favicon'] as string | undefined,
131
+ nav: normalizeNavItems(raw['nav']),
132
+ defaultAuthorId: raw['defaultAuthorId'] as string | undefined,
133
+ defaultSeo: normalizeSeoMeta(raw['defaultSeo']),
134
+ pages: {
135
+ home: normalizeHomePageConfig(pages?.['home']),
136
+ about: normalizeAboutPageConfig(pages?.['about']),
137
+ links: normalizeLinksPageConfig(pages?.['links']),
138
+ },
139
+ setupComplete: raw['setupComplete'] as boolean | undefined,
140
+ setupAcknowledgedSteps: Array.isArray(raw['setupAcknowledgedSteps'])
141
+ ? (raw['setupAcknowledgedSteps'] as string[])
142
+ : undefined,
143
+ updatedAt: normalizeTimestamp(raw['updatedAt']),
144
+ };
145
+ }
@@ -0,0 +1,65 @@
1
+ import type { Timestamp } from 'firebase/firestore';
2
+ import type { BlogPost } from '../src/lib/models/post.model';
3
+
4
+ /**
5
+ * Opaque object provided for the FIRESTORE injection token in tests.
6
+ * The actual firebase/firestore module-level functions are mocked via
7
+ * vi.mock('firebase/firestore') in each spec file.
8
+ */
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ export const firestoreStub: any = {};
11
+
12
+ /**
13
+ * Builds a minimal DocumentSnapshot-shaped object for use with mocked getDoc.
14
+ * Pass null for data to simulate a non-existent document.
15
+ */
16
+ export function mockDocSnapshot(
17
+ id: string,
18
+ data: Record<string, unknown> | null,
19
+ ) {
20
+ return {
21
+ id,
22
+ exists: () => data !== null,
23
+ data: () => data ?? undefined,
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Builds a minimal QuerySnapshot-shaped object for use with mocked getDocs.
29
+ */
30
+ export function mockQuerySnapshot(
31
+ docs: Array<{ id: string; data: Record<string, unknown> }>,
32
+ ) {
33
+ return {
34
+ empty: docs.length === 0,
35
+ docs: docs.map((d) => ({
36
+ id: d.id,
37
+ data: () => d.data,
38
+ })),
39
+ };
40
+ }
41
+
42
+ /** Fake Timestamp used throughout tests (no Firebase SDK initialisation needed). */
43
+ export const fakeTimestamp = { seconds: 1_700_000_000, nanoseconds: 0 } as unknown as Timestamp;
44
+
45
+ /**
46
+ * Factory for a minimal valid BlogPost. Spread overrides on top.
47
+ */
48
+ export function makeBlogPost(overrides: Partial<BlogPost> = {}): BlogPost {
49
+ return {
50
+ id: 'post-1',
51
+ slug: 'test-post',
52
+ title: 'Test Post',
53
+ subtitle: '',
54
+ status: 'draft',
55
+ content: '',
56
+ excerpt: '',
57
+ tags: [],
58
+ embeddedMedia: {},
59
+ seo: {},
60
+ publishedAt: fakeTimestamp.seconds * 1000,
61
+ updatedAt: fakeTimestamp.seconds * 1000,
62
+ createdAt: fakeTimestamp.seconds * 1000,
63
+ ...overrides,
64
+ };
65
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "isolatedModules": true,
5
+ "target": "es2022",
6
+ "moduleResolution": "bundler",
7
+ "strict": true,
8
+ "noImplicitOverride": true,
9
+ "noPropertyAccessFromIndexSignature": true,
10
+ "noImplicitReturns": true,
11
+ "noFallthroughCasesInSwitch": true,
12
+ "emitDecoratorMetadata": false,
13
+ "module": "preserve"
14
+ },
15
+ "angularCompilerOptions": {
16
+ "enableI18nLegacyMessageIdFormat": false,
17
+ "strictInjectionParameters": true,
18
+ "strictInputAccessModifiers": true,
19
+ "strictTemplates": true
20
+ },
21
+ "files": [],
22
+ "include": [],
23
+ "references": [
24
+ {
25
+ "path": "./tsconfig.lib.json"
26
+ },
27
+ {
28
+ "path": "./tsconfig.spec.json"
29
+ }
30
+ ]
31
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../dist/out-tsc",
5
+ "declaration": true,
6
+ "declarationMap": true,
7
+ "inlineSources": true,
8
+ "types": []
9
+ },
10
+ "include": ["src/**/*.ts"],
11
+ "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]
12
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./tsconfig.lib.json",
3
+ "compilerOptions": {
4
+ "declarationMap": false
5
+ },
6
+ "angularCompilerOptions": {
7
+ "compilationMode": "partial"
8
+ }
9
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../dist/out-tsc",
5
+ "types": ["vitest/globals"]
6
+ },
7
+ "include": ["src/**/*.ts", "src/**/*.d.ts", "testing/**/*.ts"]
8
+ }
@@ -1,5 +0,0 @@
1
- /**
2
- * Generated bundle index. Do not edit.
3
- */
4
- export * from './index';
5
- //# sourceMappingURL=foliokit-cms-core.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"foliokit-cms-core.js","sourceRoot":"","sources":["../../../../libs/cms-core/src/foliokit-cms-core.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,SAAS,CAAC","sourcesContent":["/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../libs/cms-core/src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gCAAgC,CAAC;AAC/C,cAAc,mCAAmC,CAAC;AAClD,iGAAiG;AACjG,cAAc,yBAAyB,CAAC;AACxC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oCAAoC,CAAC;AACnD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,iCAAiC,CAAC;AAChD,cAAc,iCAAiC,CAAC","sourcesContent":["export * from './lib/firebase/firebase.config';\nexport * from './lib/firebase/firebase.providers';\n// firebase-admin.ts is intentionally excluded — server-only, import directly in SSR server files\nexport * from './lib/models/post.model';\nexport * from './lib/models/site-config.model';\nexport * from './lib/models/page.model';\nexport * from './lib/models/tag.model';\nexport * from './lib/models/author.model';\nexport * from './lib/services/auth.service';\nexport * from './lib/services/post.service';\nexport * from './lib/services/page.service';\nexport * from './lib/services/site-config.service';\nexport * from './lib/services/tag.service';\nexport * from './lib/tokens/post-service.token';\nexport * from './lib/tokens/page-service.token';\n"]}
@@ -1,8 +0,0 @@
1
- import { InjectionToken } from '@angular/core';
2
- export const FIREBASE_OPTIONS = new InjectionToken('FIREBASE_OPTIONS');
3
- export const FIREBASE_APP = new InjectionToken('FIREBASE_APP');
4
- export const FIRESTORE = new InjectionToken('FIRESTORE');
5
- export const FIREBASE_STORAGE = new InjectionToken('FIREBASE_STORAGE');
6
- export const FIREBASE_AUTH = new InjectionToken('FIREBASE_AUTH');
7
- export const ADMIN_EMAIL = new InjectionToken('ADMIN_EMAIL');
8
- //# sourceMappingURL=firebase.config.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"firebase.config.js","sourceRoot":"","sources":["../../../../../../libs/cms-core/src/lib/firebase/firebase.config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAM/C,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,cAAc,CAAkB,kBAAkB,CAAC,CAAC;AACxF,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,cAAc,CAAc,cAAc,CAAC,CAAC;AAC5E,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,cAAc,CAAY,WAAW,CAAC,CAAC;AACpE,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,cAAc,CAAkB,kBAAkB,CAAC,CAAC;AACxF,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,cAAc,CAAc,eAAe,CAAC,CAAC;AAC9E,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,cAAc,CAAS,aAAa,CAAC,CAAC","sourcesContent":["import { InjectionToken } from '@angular/core';\nimport type { FirebaseApp, FirebaseOptions } from 'firebase/app';\nimport type { Firestore } from 'firebase/firestore';\nimport type { FirebaseStorage } from 'firebase/storage';\nimport type { Auth } from 'firebase/auth';\n\nexport const FIREBASE_OPTIONS = new InjectionToken<FirebaseOptions>('FIREBASE_OPTIONS');\nexport const FIREBASE_APP = new InjectionToken<FirebaseApp>('FIREBASE_APP');\nexport const FIRESTORE = new InjectionToken<Firestore>('FIRESTORE');\nexport const FIREBASE_STORAGE = new InjectionToken<FirebaseStorage>('FIREBASE_STORAGE');\nexport const FIREBASE_AUTH = new InjectionToken<Auth | null>('FIREBASE_AUTH');\nexport const ADMIN_EMAIL = new InjectionToken<string>('ADMIN_EMAIL');\n"]}
@@ -1,54 +0,0 @@
1
- import { PLATFORM_ID, inject, makeEnvironmentProviders, } from '@angular/core';
2
- import { isPlatformBrowser } from '@angular/common';
3
- import { getApp, getApps, initializeApp } from 'firebase/app';
4
- import { connectFirestoreEmulator, getFirestore, initializeFirestore, memoryLocalCache, } from 'firebase/firestore';
5
- import { getStorage } from 'firebase/storage';
6
- import { getAuth } from 'firebase/auth';
7
- import { FIREBASE_APP, FIREBASE_AUTH, FIREBASE_OPTIONS, FIREBASE_STORAGE, FIRESTORE, } from './firebase.config';
8
- export function provideFirebase(options, useEmulator = false) {
9
- return makeEnvironmentProviders([
10
- {
11
- provide: FIREBASE_OPTIONS,
12
- useValue: options,
13
- },
14
- {
15
- // Firebase app is a Node.js-process-level singleton. In SSR, each request
16
- // creates a new DI context but shares the same global Firebase registry, so
17
- // we must reuse the existing app rather than calling initializeApp again.
18
- provide: FIREBASE_APP,
19
- useFactory: () => getApps().length ? getApp() : initializeApp(inject(FIREBASE_OPTIONS)),
20
- },
21
- {
22
- // Same singleton constraint applies to the Firestore instance.
23
- // initializeFirestore throws if called a second time for the same app, so
24
- // fall back to getFirestore() which returns the already-configured instance.
25
- provide: FIRESTORE,
26
- useFactory: () => {
27
- const app = inject(FIREBASE_APP);
28
- try {
29
- const db = initializeFirestore(app, { localCache: memoryLocalCache() });
30
- if (useEmulator) {
31
- connectFirestoreEmulator(db, '127.0.0.1', 8080);
32
- }
33
- return db;
34
- }
35
- catch {
36
- return getFirestore(app);
37
- }
38
- },
39
- },
40
- {
41
- provide: FIREBASE_STORAGE,
42
- useFactory: () => getStorage(inject(FIREBASE_APP)),
43
- },
44
- {
45
- // Auth relies on browser APIs (IndexedDB, localStorage) — null on server
46
- provide: FIREBASE_AUTH,
47
- useFactory: () => {
48
- const platformId = inject(PLATFORM_ID);
49
- return isPlatformBrowser(platformId) ? getAuth(inject(FIREBASE_APP)) : null;
50
- },
51
- },
52
- ]);
53
- }
54
- //# sourceMappingURL=firebase.providers.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"firebase.providers.js","sourceRoot":"","sources":["../../../../../../libs/cms-core/src/lib/firebase/firebase.providers.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,WAAW,EACX,MAAM,EACN,wBAAwB,GACzB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAmB,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,EACL,wBAAwB,EACxB,YAAY,EACZ,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EACL,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,gBAAgB,EAChB,SAAS,GACV,MAAM,mBAAmB,CAAC;AAE3B,MAAM,UAAU,eAAe,CAC7B,OAAwB,EACxB,WAAW,GAAG,KAAK;IAEnB,OAAO,wBAAwB,CAAC;QAC9B;YACE,OAAO,EAAE,gBAAgB;YACzB,QAAQ,EAAE,OAAO;SAClB;QACD;YACE,0EAA0E;YAC1E,4EAA4E;YAC5E,0EAA0E;YAC1E,OAAO,EAAE,YAAY;YACrB,UAAU,EAAE,GAAG,EAAE,CACf,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;SACxE;QACD;YACE,+DAA+D;YAC/D,0EAA0E;YAC1E,6EAA6E;YAC7E,OAAO,EAAE,SAAS;YAClB,UAAU,EAAE,GAAG,EAAE;gBACf,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;gBACjC,IAAI,CAAC;oBACH,MAAM,EAAE,GAAG,mBAAmB,CAAC,GAAG,EAAE,EAAE,UAAU,EAAE,gBAAgB,EAAE,EAAE,CAAC,CAAC;oBACxE,IAAI,WAAW,EAAE,CAAC;wBAChB,wBAAwB,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,CAAC,CAAC;oBAClD,CAAC;oBACD,OAAO,EAAE,CAAC;gBACZ,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,YAAY,CAAC,GAAG,CAAC,CAAC;gBAC3B,CAAC;YACH,CAAC;SACF;QACD;YACE,OAAO,EAAE,gBAAgB;YACzB,UAAU,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;SACnD;QACD;YACE,yEAAyE;YACzE,OAAO,EAAE,aAAa;YACtB,UAAU,EAAE,GAAG,EAAE;gBACf,MAAM,UAAU,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;gBACvC,OAAO,iBAAiB,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAC9E,CAAC;SACF;KACF,CAAC,CAAC;AACL,CAAC","sourcesContent":["import {\n EnvironmentProviders,\n PLATFORM_ID,\n inject,\n makeEnvironmentProviders,\n} from '@angular/core';\nimport { isPlatformBrowser } from '@angular/common';\nimport { FirebaseOptions, getApp, getApps, initializeApp } from 'firebase/app';\nimport {\n connectFirestoreEmulator,\n getFirestore,\n initializeFirestore,\n memoryLocalCache,\n} from 'firebase/firestore';\nimport { getStorage } from 'firebase/storage';\nimport { getAuth } from 'firebase/auth';\nimport {\n FIREBASE_APP,\n FIREBASE_AUTH,\n FIREBASE_OPTIONS,\n FIREBASE_STORAGE,\n FIRESTORE,\n} from './firebase.config';\n\nexport function provideFirebase(\n options: FirebaseOptions,\n useEmulator = false\n): EnvironmentProviders {\n return makeEnvironmentProviders([\n {\n provide: FIREBASE_OPTIONS,\n useValue: options,\n },\n {\n // Firebase app is a Node.js-process-level singleton. In SSR, each request\n // creates a new DI context but shares the same global Firebase registry, so\n // we must reuse the existing app rather than calling initializeApp again.\n provide: FIREBASE_APP,\n useFactory: () =>\n getApps().length ? getApp() : initializeApp(inject(FIREBASE_OPTIONS)),\n },\n {\n // Same singleton constraint applies to the Firestore instance.\n // initializeFirestore throws if called a second time for the same app, so\n // fall back to getFirestore() which returns the already-configured instance.\n provide: FIRESTORE,\n useFactory: () => {\n const app = inject(FIREBASE_APP);\n try {\n const db = initializeFirestore(app, { localCache: memoryLocalCache() });\n if (useEmulator) {\n connectFirestoreEmulator(db, '127.0.0.1', 8080);\n }\n return db;\n } catch {\n return getFirestore(app);\n }\n },\n },\n {\n provide: FIREBASE_STORAGE,\n useFactory: () => getStorage(inject(FIREBASE_APP)),\n },\n {\n // Auth relies on browser APIs (IndexedDB, localStorage) — null on server\n provide: FIREBASE_AUTH,\n useFactory: () => {\n const platformId = inject(PLATFORM_ID);\n return isPlatformBrowser(platformId) ? getAuth(inject(FIREBASE_APP)) : null;\n },\n },\n ]);\n}\n"]}
@@ -1 +0,0 @@
1
- //# sourceMappingURL=author.model.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"author.model.js","sourceRoot":"","sources":["../../../../../../libs/cms-core/src/lib/models/author.model.ts"],"names":[],"mappings":"","sourcesContent":["import type { SocialLink } from './site-config.model';\n\nexport interface Author {\n id: string;\n name: string;\n slug: string;\n bio?: string;\n avatarUrl?: string;\n email?: string;\n social?: SocialLink[];\n /** Unix milliseconds. */\n createdAt: number;\n /** Unix milliseconds. */\n updatedAt: number;\n}\n"]}
@@ -1 +0,0 @@
1
- //# sourceMappingURL=page.model.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"page.model.js","sourceRoot":"","sources":["../../../../../../libs/cms-core/src/lib/models/page.model.ts"],"names":[],"mappings":"","sourcesContent":["import type { SeoMeta, EmbeddedMediaEntry } from './post.model';\nimport type { SocialPlatform } from './site-config.model';\n\nexport interface CmsPageBase {\n id: string;\n type: 'about' | 'links';\n slug: string;\n title: string;\n status: 'draft' | 'published';\n seo: SeoMeta;\n /** Unix milliseconds — same convention as BlogPost. */\n updatedAt: number;\n /** Unix milliseconds. */\n createdAt: number;\n}\n\nexport interface AboutPage extends CmsPageBase {\n type: 'about';\n heroImageUrl?: string;\n heroImageAlt?: string;\n body: string;\n contentVersion: number;\n embeddedMedia: Record<string, EmbeddedMediaEntry>;\n}\n\nexport interface LinksLink {\n id: string;\n label: string;\n url: string;\n icon?: string;\n platform?: SocialPlatform;\n highlighted?: boolean;\n order: number;\n}\n\nexport interface LinksPage extends CmsPageBase {\n type: 'links';\n avatarUrl?: string;\n avatarAlt?: string;\n headline?: string;\n bio?: string;\n links: LinksLink[];\n}\n\nexport type CmsPageUnion = AboutPage | LinksPage;\n"]}
@@ -1 +0,0 @@
1
- //# sourceMappingURL=post.model.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"post.model.js","sourceRoot":"","sources":["../../../../../../libs/cms-core/src/lib/models/post.model.ts"],"names":[],"mappings":"","sourcesContent":["export interface SeoMeta {\n title?: string;\n description?: string;\n keywords?: string[];\n ogImage?: string;\n canonicalUrl?: string;\n noIndex?: boolean;\n}\n\nexport interface EmbeddedMediaEntry {\n token: string;\n storagePath: string;\n downloadUrl: string;\n alt: string;\n mimeType: string;\n}\n\nexport interface BlogPost {\n id: string;\n slug: string;\n title: string;\n subtitle?: string;\n status: 'published' | 'draft' | 'scheduled' | 'archived';\n content: string;\n excerpt?: string;\n thumbnailUrl?: string;\n thumbnailAlt?: string;\n tags: string[];\n authorId?: string;\n readingTimeMinutes?: number;\n embeddedMedia: Record<string, EmbeddedMediaEntry>;\n seo: SeoMeta;\n /** Unix milliseconds. Stored as Firestore Timestamp but always normalized on read. */\n publishedAt: number;\n /** Unix milliseconds, optional. */\n scheduledPublishAt?: number;\n /** Unix milliseconds. */\n updatedAt: number;\n /** Unix milliseconds. */\n createdAt: number;\n}\n"]}
@@ -1 +0,0 @@
1
- //# sourceMappingURL=site-config.model.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"site-config.model.js","sourceRoot":"","sources":["../../../../../../libs/cms-core/src/lib/models/site-config.model.ts"],"names":[],"mappings":"","sourcesContent":["export interface NavItem {\n label: string;\n url: string;\n order?: number;\n external?: boolean;\n icon?: string;\n}\n\nexport type SocialPlatform =\n | 'twitter'\n | 'instagram'\n | 'github'\n | 'linkedin'\n | 'youtube'\n | 'twitch'\n | 'bluesky'\n | 'tiktok'\n | 'facebook'\n | 'email'\n | 'website';\n\nexport interface SocialLink {\n platform: SocialPlatform;\n url: string;\n label?: string;\n icon?: string;\n}\n\nexport interface SiteConfig {\n id: string;\n siteName: string;\n siteUrl: string;\n description?: string;\n logo?: string;\n favicon?: string;\n nav: NavItem[];\n social: SocialLink[];\n defaultAuthorId?: string;\n /** Unix milliseconds. */\n updatedAt: number;\n}\n"]}
@@ -1 +0,0 @@
1
- //# sourceMappingURL=tag.model.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"tag.model.js","sourceRoot":"","sources":["../../../../../../libs/cms-core/src/lib/models/tag.model.ts"],"names":[],"mappings":"","sourcesContent":["export interface Tag {\n id: string;\n label: string;\n slug: string;\n}\n"]}
@@ -1,42 +0,0 @@
1
- import { computed, inject, Injectable } from '@angular/core';
2
- import { toSignal } from '@angular/core/rxjs-interop';
3
- import { Observable } from 'rxjs';
4
- import { GoogleAuthProvider, onAuthStateChanged, signInWithPopup, signOut, } from 'firebase/auth';
5
- import { ADMIN_EMAIL, FIREBASE_AUTH } from '../firebase/firebase.config';
6
- import * as i0 from "@angular/core";
7
- export class AuthService {
8
- auth = inject(FIREBASE_AUTH);
9
- adminEmail = inject(ADMIN_EMAIL, { optional: true });
10
- user = toSignal(new Observable((subscriber) => {
11
- if (!this.auth) {
12
- subscriber.next(null);
13
- subscriber.complete();
14
- return;
15
- }
16
- return onAuthStateChanged(this.auth, (u) => subscriber.next(u));
17
- }), { requireSync: false, initialValue: undefined });
18
- isAuthenticated = computed(() => this.user() != null, ...(ngDevMode ? [{ debugName: "isAuthenticated" }] : /* istanbul ignore next */ []));
19
- isAdmin = computed(() => {
20
- const email = this.user()?.email;
21
- if (!email)
22
- return false;
23
- return this.adminEmail ? email === this.adminEmail : false;
24
- }, ...(ngDevMode ? [{ debugName: "isAdmin" }] : /* istanbul ignore next */ []));
25
- async signInWithGoogle() {
26
- if (!this.auth)
27
- return;
28
- await signInWithPopup(this.auth, new GoogleAuthProvider());
29
- }
30
- async signOut() {
31
- if (!this.auth)
32
- return;
33
- await signOut(this.auth);
34
- }
35
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: AuthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
36
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: AuthService, providedIn: 'root' });
37
- }
38
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: AuthService, decorators: [{
39
- type: Injectable,
40
- args: [{ providedIn: 'root' }]
41
- }] });
42
- //# sourceMappingURL=auth.service.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"auth.service.js","sourceRoot":"","sources":["../../../../../../libs/cms-core/src/lib/services/auth.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAClC,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,eAAe,EACf,OAAO,GAER,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;;AAGzE,MAAM,OAAO,WAAW;IACL,IAAI,GAAG,MAAM,CAAC,aAAa,CAAC,CAAC;IAC7B,UAAU,GAAG,MAAM,CAAC,WAAW,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IAE7D,IAAI,GAAG,QAAQ,CACtB,IAAI,UAAU,CAAc,CAAC,UAAU,EAAE,EAAE;QACzC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YACf,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACtB,UAAU,CAAC,QAAQ,EAAE,CAAC;YACtB,OAAO;QACT,CAAC;QACD,OAAO,kBAAkB,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAClE,CAAC,CAAC,EACF,EAAE,WAAW,EAAE,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,CAChD,CAAC;IAEO,eAAe,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,IAAI,sFAAC,CAAC;IAEtD,OAAO,GAAG,QAAQ,CAAC,GAAG,EAAE;QAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,EAAE,KAAK,CAAC;QACjC,IAAI,CAAC,KAAK;YAAE,OAAO,KAAK,CAAC;QACzB,OAAO,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC;IAC7D,CAAC,8EAAC,CAAC;IAEH,KAAK,CAAC,gBAAgB;QACpB,IAAI,CAAC,IAAI,CAAC,IAAI;YAAE,OAAO;QACvB,MAAM,eAAe,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,kBAAkB,EAAE,CAAC,CAAC;IAC7D,CAAC;IAED,KAAK,CAAC,OAAO;QACX,IAAI,CAAC,IAAI,CAAC,IAAI;YAAE,OAAO;QACvB,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC;uGAhCU,WAAW;2GAAX,WAAW,cADE,MAAM;;2FACnB,WAAW;kBADvB,UAAU;mBAAC,EAAE,UAAU,EAAE,MAAM,EAAE","sourcesContent":["import { computed, inject, Injectable } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { Observable } from 'rxjs';\nimport {\n GoogleAuthProvider,\n onAuthStateChanged,\n signInWithPopup,\n signOut,\n User,\n} from 'firebase/auth';\nimport { ADMIN_EMAIL, FIREBASE_AUTH } from '../firebase/firebase.config';\n\n@Injectable({ providedIn: 'root' })\nexport class AuthService {\n private readonly auth = inject(FIREBASE_AUTH);\n private readonly adminEmail = inject(ADMIN_EMAIL, { optional: true });\n\n readonly user = toSignal(\n new Observable<User | null>((subscriber) => {\n if (!this.auth) {\n subscriber.next(null);\n subscriber.complete();\n return;\n }\n return onAuthStateChanged(this.auth, (u) => subscriber.next(u));\n }),\n { requireSync: false, initialValue: undefined },\n );\n\n readonly isAuthenticated = computed(() => this.user() != null);\n\n readonly isAdmin = computed(() => {\n const email = this.user()?.email;\n if (!email) return false;\n return this.adminEmail ? email === this.adminEmail : false;\n });\n\n async signInWithGoogle(): Promise<void> {\n if (!this.auth) return;\n await signInWithPopup(this.auth, new GoogleAuthProvider());\n }\n\n async signOut(): Promise<void> {\n if (!this.auth) return;\n await signOut(this.auth);\n }\n}\n"]}
@@ -1,73 +0,0 @@
1
- import { inject, Injectable } from '@angular/core';
2
- import { collection, doc, getDoc, getDocs, limit, orderBy, query, setDoc, Timestamp, updateDoc, where, } from 'firebase/firestore';
3
- import { deleteObject, ref } from 'firebase/storage';
4
- import { from, of } from 'rxjs';
5
- import { catchError, map } from 'rxjs/operators';
6
- import { FIREBASE_STORAGE, FIRESTORE } from '../firebase/firebase.config';
7
- import { normalizePage } from '../utils/normalize-page';
8
- import * as i0 from "@angular/core";
9
- function omitUndefined(obj) {
10
- return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
11
- }
12
- export class PageService {
13
- firestore = inject(FIRESTORE);
14
- storage = inject(FIREBASE_STORAGE);
15
- getPageBySlug(slug) {
16
- const q = query(collection(this.firestore, 'pages'), where('status', '==', 'published'), where('slug', '==', slug), limit(1));
17
- return from(getDocs(q)).pipe(map((snapshot) => {
18
- if (snapshot.empty)
19
- return null;
20
- const d = snapshot.docs[0];
21
- return normalizePage({ id: d.id, ...d.data() });
22
- }), catchError((err) => {
23
- console.error('[PageService.getPageBySlug]', err);
24
- return of(null);
25
- }));
26
- }
27
- getAllPages() {
28
- const q = query(collection(this.firestore, 'pages'), orderBy('updatedAt', 'desc'));
29
- return from(getDocs(q)).pipe(map((snapshot) => snapshot.docs.map((d) => normalizePage({ id: d.id, ...d.data() }))), catchError((err) => {
30
- console.error('[PageService.getAllPages]', err);
31
- return of([]);
32
- }));
33
- }
34
- getPageById(id) {
35
- return from(getDoc(doc(this.firestore, 'pages', id))).pipe(map((snapshot) => {
36
- if (!snapshot.exists())
37
- throw new Error(`Page not found: ${id}`);
38
- return normalizePage({ id: snapshot.id, ...snapshot.data() });
39
- }));
40
- }
41
- deleteStorageFile(storagePath) {
42
- const fileRef = ref(this.storage, storagePath);
43
- return from(deleteObject(fileRef));
44
- }
45
- savePage(page) {
46
- const nowMs = Date.now();
47
- const nowTs = Timestamp.fromMillis(nowMs);
48
- if (page.id === '') {
49
- const newId = page.type === 'about' || page.type === 'links'
50
- ? page.type
51
- : doc(collection(this.firestore, 'pages')).id;
52
- const savedPage = { ...page, id: newId, createdAt: nowMs, updatedAt: nowMs };
53
- const firestorePayload = omitUndefined({ ...savedPage, createdAt: nowTs, updatedAt: nowTs });
54
- return from(setDoc(doc(this.firestore, 'pages', newId), firestorePayload)).pipe(map(() => savedPage), catchError((err) => {
55
- console.error('[PageService.savePage/create]', err);
56
- throw err;
57
- }));
58
- }
59
- const savedPage = { ...page, updatedAt: nowMs };
60
- const firestorePayload = omitUndefined({ ...savedPage, updatedAt: nowTs });
61
- return from(updateDoc(doc(this.firestore, 'pages', page.id), firestorePayload)).pipe(map(() => savedPage), catchError((err) => {
62
- console.error('[PageService.savePage/update]', err);
63
- throw err;
64
- }));
65
- }
66
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
67
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PageService, providedIn: 'root' });
68
- }
69
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PageService, decorators: [{
70
- type: Injectable,
71
- args: [{ providedIn: 'root' }]
72
- }] });
73
- //# sourceMappingURL=page.service.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"page.service.js","sourceRoot":"","sources":["../../../../../../libs/cms-core/src/lib/services/page.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { inject, Injectable } from '@angular/core';\nimport {\n collection,\n doc,\n getDoc,\n getDocs,\n limit,\n orderBy,\n query,\n setDoc,\n Timestamp,\n updateDoc,\n where,\n} from 'firebase/firestore';\nimport { deleteObject, ref } from 'firebase/storage';\nimport { from, Observable, of } from 'rxjs';\nimport { catchError, map } from 'rxjs/operators';\nimport { FIREBASE_STORAGE, FIRESTORE } from '../firebase/firebase.config';\nimport type { CmsPageUnion } from '../models/page.model';\nimport { normalizePage } from '../utils/normalize-page';\nimport type { IPageService } from '../tokens/page-service.token';\n\nfunction omitUndefined(obj: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));\n}\n\n@Injectable({ providedIn: 'root' })\nexport class PageService implements IPageService {\n private readonly firestore = inject(FIRESTORE);\n private readonly storage = inject(FIREBASE_STORAGE);\n\n getPageBySlug(slug: string): Observable<CmsPageUnion | null> {\n const q = query(\n collection(this.firestore, 'pages'),\n where('status', '==', 'published'),\n where('slug', '==', slug),\n limit(1),\n );\n return from(getDocs(q)).pipe(\n map((snapshot) => {\n if (snapshot.empty) return null;\n const d = snapshot.docs[0];\n return normalizePage({ id: d.id, ...d.data() });\n }),\n catchError((err) => {\n console.error('[PageService.getPageBySlug]', err);\n return of(null);\n }),\n );\n }\n\n getAllPages(): Observable<CmsPageUnion[]> {\n const q = query(\n collection(this.firestore, 'pages'),\n orderBy('updatedAt', 'desc'),\n );\n return from(getDocs(q)).pipe(\n map((snapshot) =>\n snapshot.docs.map((d) => normalizePage({ id: d.id, ...d.data() })),\n ),\n catchError((err) => {\n console.error('[PageService.getAllPages]', err);\n return of([]);\n }),\n );\n }\n\n getPageById(id: string): Observable<CmsPageUnion> {\n return from(getDoc(doc(this.firestore, 'pages', id))).pipe(\n map((snapshot) => {\n if (!snapshot.exists()) throw new Error(`Page not found: ${id}`);\n return normalizePage({ id: snapshot.id, ...snapshot.data() });\n }),\n );\n }\n\n deleteStorageFile(storagePath: string): Observable<void> {\n const fileRef = ref(this.storage, storagePath);\n return from(deleteObject(fileRef));\n }\n\n savePage(page: CmsPageUnion): Observable<CmsPageUnion> {\n const nowMs = Date.now();\n const nowTs = Timestamp.fromMillis(nowMs);\n\n if (page.id === '') {\n const newId =\n page.type === 'about' || page.type === 'links'\n ? page.type\n : doc(collection(this.firestore, 'pages')).id;\n const savedPage: CmsPageUnion = { ...page, id: newId, createdAt: nowMs, updatedAt: nowMs };\n const firestorePayload = omitUndefined({ ...savedPage, createdAt: nowTs, updatedAt: nowTs });\n return from(\n setDoc(doc(this.firestore, 'pages', newId), firestorePayload),\n ).pipe(\n map(() => savedPage),\n catchError((err) => {\n console.error('[PageService.savePage/create]', err);\n throw err;\n }),\n );\n }\n\n const savedPage: CmsPageUnion = { ...page, updatedAt: nowMs };\n const firestorePayload = omitUndefined({ ...savedPage, updatedAt: nowTs });\n return from(\n updateDoc(doc(this.firestore, 'pages', page.id), firestorePayload),\n ).pipe(\n map(() => savedPage),\n catchError((err) => {\n console.error('[PageService.savePage/update]', err);\n throw err;\n }),\n );\n }\n}\n"]}