|
1 |
| -import { Component, Input, HostListener, ViewEncapsulation } from '@angular/core'; |
| 1 | +import { Component, Input, HostListener, ViewEncapsulation, PLATFORM_ID, Inject } from '@angular/core'; |
2 | 2 | import { ContentFile } from '@analogjs/content';
|
3 |
| -import { Observable } from 'rxjs'; |
| 3 | +import { filter, Observable, Subscription } from 'rxjs'; |
4 | 4 | import { MarkdownComponent } from '@analogjs/content';
|
5 | 5 | import { PrimeNgModule } from '~/app/prime-ng.module';
|
6 | 6 | import { MetaTagsService } from '~/app/services/meta-tags.service';
|
7 |
| -import { CommonModule, NgIf } from '@angular/common'; |
| 7 | +import { CommonModule, isPlatformBrowser, NgIf } from '@angular/common'; |
| 8 | +import { NavigationEnd, Router } from '@angular/router'; |
| 9 | +import { ErrorHandlerService } from '~/app/services/error-handler.service'; |
8 | 10 |
|
9 | 11 | export interface DocAttributes {
|
10 | 12 | title: string;
|
@@ -88,48 +90,160 @@ export class DocsViewerComponent {
|
88 | 90 |
|
89 | 91 | doc: ContentFile<DocAttributes | Record<string, never>> | null = null;
|
90 | 92 |
|
91 |
| - navTop = 'unset'; // default top offset |
| 93 | + navTop = 'unset'; |
| 94 | + private docSub?: Subscription; |
| 95 | + private routerSub?: Subscription; |
| 96 | + private docLoaded = false; |
| 97 | + private hasRenderedMermaid = false; |
92 | 98 |
|
93 |
| - constructor(private metaTagsService: MetaTagsService) {} |
| 99 | + constructor( |
| 100 | + private router: Router, |
| 101 | + private errorHandler: ErrorHandlerService, |
| 102 | + private metaTagsService: MetaTagsService, |
| 103 | + @Inject(PLATFORM_ID) private platformId: Object |
| 104 | + ) {} |
94 | 105 |
|
95 | 106 | ngOnInit() {
|
96 |
| - this.doc$.subscribe(doc => { |
97 |
| - // Set current doc when it resolves |
98 |
| - this.doc = doc; |
99 |
| - // If doc has attributes, then get them for meta and JSON-LD content |
100 |
| - if (doc?.attributes) { |
101 |
| - const { title, description, coverImage, author, publishedDate, modifiedDate, slug } = doc.attributes; |
102 |
| - |
103 |
| - // Set meta tags |
104 |
| - this.metaTagsService.setCustomMeta(title, description, undefined, coverImage || this.getFallbackImage(title)); |
105 |
| - |
106 |
| - // Set JSON-LD structured data |
107 |
| - this.metaTagsService.addStructuredData('article', { |
108 |
| - title: title, |
109 |
| - description: description, |
110 |
| - coverImage: coverImage || this.getFallbackImage(title), |
111 |
| - author: author || 'Domain Locker Team', |
112 |
| - publishedDate: publishedDate || new Date().toISOString(), |
113 |
| - modifiedDate: modifiedDate || publishedDate || new Date().toISOString(), |
114 |
| - slug: slug, |
115 |
| - category: this.categoryName, |
116 |
| - }); |
| 107 | + this.docSub = this.doc$.subscribe({ |
| 108 | + next: (doc) => { |
| 109 | + // Set current doc when it resolves |
| 110 | + this.doc = doc; |
| 111 | + // If doc has attributes, then get them for meta and JSON-LD content |
| 112 | + if (doc?.attributes) { |
| 113 | + const { title, description, coverImage, author, publishedDate, modifiedDate, slug } = doc.attributes; |
| 114 | + |
| 115 | + // Set meta tags |
| 116 | + this.metaTagsService.setCustomMeta(title, description, undefined, coverImage || this.getFallbackImage(title)); |
| 117 | + |
| 118 | + // Set JSON-LD structured data |
| 119 | + this.metaTagsService.addStructuredData('article', { |
| 120 | + title: title, |
| 121 | + description: description, |
| 122 | + coverImage: coverImage || this.getFallbackImage(title), |
| 123 | + author: author || 'Domain Locker Team', |
| 124 | + publishedDate: publishedDate || new Date().toISOString(), |
| 125 | + modifiedDate: modifiedDate || publishedDate || new Date().toISOString(), |
| 126 | + slug: slug, |
| 127 | + category: this.categoryName, |
| 128 | + }); |
| 129 | + } |
| 130 | + if (isPlatformBrowser(this.platformId)) { |
| 131 | + setTimeout(() => { |
| 132 | + this.loadAndRenderMermaid(); |
| 133 | + }, 50); |
| 134 | + } |
| 135 | + this.docLoaded = true; |
| 136 | + }, |
| 137 | + error: (err) => { |
| 138 | + this.errorHandler.handleError({ error: err, message: 'Doc subscription error', location: 'doc-viewer' }); |
117 | 139 | }
|
118 | 140 | });
|
| 141 | + |
| 142 | + this.routerSub = this.router.events |
| 143 | + .pipe(filter((e) => e instanceof NavigationEnd)) |
| 144 | + .subscribe({ |
| 145 | + next: () => { |
| 146 | + if (isPlatformBrowser(this.platformId)) { |
| 147 | + setTimeout(() => this.loadAndRenderMermaid(), 50); |
| 148 | + } |
| 149 | + }, |
| 150 | + error: (err) => { |
| 151 | + this.errorHandler.handleError({ error: err, message: 'Router events error', location: 'doc-viewer' }); |
| 152 | + } |
| 153 | + }); |
| 154 | + } |
| 155 | + |
| 156 | + ngOnDestroy(): void { |
| 157 | + if (this.docSub) { |
| 158 | + try { |
| 159 | + this.docSub.unsubscribe(); |
| 160 | + } catch (err) { |
| 161 | + this.errorHandler.handleError({ error: err, message: 'Doc subscription cleanup error', location: 'doc-viewer' }); |
| 162 | + } |
| 163 | + } |
| 164 | + if (this.routerSub) { |
| 165 | + try { |
| 166 | + this.routerSub.unsubscribe(); |
| 167 | + } catch (err) { |
| 168 | + this.errorHandler.handleError({ error: err, message: 'Router subscription cleanup error', location: 'doc-viewer' }); |
| 169 | + } |
| 170 | + } |
| 171 | + } |
| 172 | + |
| 173 | + ngAfterViewChecked(): void { |
| 174 | + // If running client-side, and doc is loaded but no mermaid rendered yet, then init |
| 175 | + if (!isPlatformBrowser(this.platformId)) return; |
| 176 | + if (this.docLoaded && !this.hasRenderedMermaid) { |
| 177 | + this.loadAndRenderMermaid(); |
| 178 | + this.hasRenderedMermaid = true; |
| 179 | + } |
119 | 180 | }
|
120 | 181 |
|
121 | 182 | /** Called on window scroll. If user scrolled > 7rem => fix nav top at 7rem. Otherwise 0. */
|
122 | 183 | @HostListener('window:scroll')
|
123 | 184 | onWindowScroll() {
|
124 |
| - const scrollY = window.scrollY; |
125 |
| - const sevenRemInPx = 112; // approx 7rem if root font-size = 16px |
126 |
| - this.navTop = scrollY > sevenRemInPx ? '1rem' : '9rem'; |
| 185 | + try { |
| 186 | + const scrollY = window.scrollY; |
| 187 | + const sevenRemInPx = 112; // approx 7rem if root font-size = 16px |
| 188 | + this.navTop = scrollY > sevenRemInPx ? '1rem' : '9rem'; |
| 189 | + } catch (err) { |
| 190 | + this.errorHandler.handleError({ error: err, message: 'Scroll handler error', location: 'doc-viewer' }); |
| 191 | + } |
127 | 192 | }
|
128 | 193 |
|
129 | 194 | getFallbackImage(title: string) {
|
130 |
| - const encodedTitle = encodeURIComponent(title); |
131 |
| - return `https://dynamic-og-image-generator.vercel.app/api/generate?title=${encodedTitle}` |
132 |
| - + ' &author=Domain+Locker&websiteUrl=domain-locker.com&avatar=https%3A%2F%2Fdomain-locker' |
133 |
| - + '.com%2Ficons%2Fandroid-chrome-maskable-192x192.png&theme=dracula'; |
| 195 | + try { |
| 196 | + const encodedTitle = encodeURIComponent(title); |
| 197 | + return `https://dynamic-og-image-generator.vercel.app/api/generate?title=${encodedTitle}` |
| 198 | + + ' &author=Domain+Locker&websiteUrl=domain-locker.com&avatar=https%3A%2F%2Fdomain-locker' |
| 199 | + + '.com%2Ficons%2Fandroid-chrome-maskable-192x192.png&theme=dracula'; |
| 200 | + } catch (err) { |
| 201 | + this.errorHandler.handleError({ error: err, message: 'Fallback image error', location: 'doc-viewer' }); |
| 202 | + return 'https://domain-locker.com/og.png'; |
| 203 | + } |
| 204 | + } |
| 205 | + |
| 206 | + /** |
| 207 | + * 1) Checks for any <pre class="mermaid"> blocks |
| 208 | + * 2) If found, dynamically load mermaid from a CDN |
| 209 | + * 3) Then call mermaid.initialize + mermaid.run |
| 210 | + */ |
| 211 | + private loadAndRenderMermaid() { |
| 212 | + try { |
| 213 | + const mermaidBlocks = document.querySelectorAll('pre.mermaid'); |
| 214 | + if (!mermaidBlocks?.length) { |
| 215 | + return; |
| 216 | + } |
| 217 | + const existingScript = document.getElementById('mermaidScript') as HTMLScriptElement | null; |
| 218 | + if (existingScript) { |
| 219 | + this.runMermaid(); |
| 220 | + } else { |
| 221 | + const script = document.createElement('script'); |
| 222 | + script.id = 'mermaidScript'; |
| 223 | + script.src = 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js'; |
| 224 | + script.async = true; |
| 225 | + script.onload = () => { |
| 226 | + this.runMermaid(); |
| 227 | + }; |
| 228 | + document.head.appendChild(script); |
| 229 | + } |
| 230 | + } catch (err) { |
| 231 | + this.errorHandler.handleError({ error: err, message: 'loadAndRenderMermaid error', location: 'doc-viewer' }); |
| 232 | + } |
| 233 | + } |
| 234 | + |
| 235 | + private runMermaid() { |
| 236 | + try { |
| 237 | + const mermaid = (window as any).mermaid; |
| 238 | + if (!mermaid) return; |
| 239 | + try { |
| 240 | + mermaid.initialize({ startOnLoad: false }); |
| 241 | + mermaid.run({ querySelector: 'pre.mermaid' }); |
| 242 | + } catch (err) { |
| 243 | + this.errorHandler.handleError({ error: err, message: 'Mermaid render failed', location: 'doc-viewer' }); |
| 244 | + } |
| 245 | + } catch (err) { |
| 246 | + this.errorHandler.handleError({ error: err, message: 'runMermaid error', location: 'doc-viewer' }); |
| 247 | + } |
134 | 248 | }
|
135 | 249 | }
|
0 commit comments