diff --git a/angular-primeng-app/src/app/components/footer/footer.component.ts b/angular-primeng-app/src/app/components/footer/footer.component.ts index 16a3543..d885a51 100644 --- a/angular-primeng-app/src/app/components/footer/footer.component.ts +++ b/angular-primeng-app/src/app/components/footer/footer.component.ts @@ -1,8 +1,9 @@ -import { Component } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { BlogService } from '../../services/blog.service'; import { Subscription } from 'rxjs'; import { ToolbarModule } from 'primeng/toolbar'; +import { BlogInfo } from '../../models/blog-info'; @Component({ selector: 'app-footer', @@ -12,17 +13,17 @@ import { ToolbarModule } from 'primeng/toolbar'; styleUrl: './footer.component.scss' }) export class FooterComponent { + blogURL!: string; + blogInfo!: BlogInfo; blogName = ''; - date = new Date().getFullYear(); + blogService: BlogService = inject(BlogService); private querySubscription?: Subscription; - constructor(private blogService: BlogService) { - } - ngOnInit() { - this.querySubscription = this.blogService.getBlogInfo() + this.blogURL = this.blogService.getBlogURL(); + this.querySubscription = this.blogService.getBlogInfo(this.blogURL) .subscribe((data) => this.blogName = data.title); } diff --git a/angular-primeng-app/src/app/components/header/header.component.html b/angular-primeng-app/src/app/components/header/header.component.html index 78cb8e3..cf00426 100644 --- a/angular-primeng-app/src/app/components/header/header.component.html +++ b/angular-primeng-app/src/app/components/header/header.component.html @@ -6,6 +6,7 @@

{{ blogName }}

+
@@ -27,7 +28,7 @@

{{ blogName }}

- +
@@ -38,12 +39,3 @@

{{ blogName }}

- - -
- -
-

Hey, 👋 sign up or sign in to interact.

- Sign inSign in with Hashnode -

This blog is powered by Hashnode. To interact with the content on this blog, please log in through Hashnode.

-
diff --git a/angular-primeng-app/src/app/components/header/header.component.scss b/angular-primeng-app/src/app/components/header/header.component.scss index 3d910f4..c908cc7 100644 --- a/angular-primeng-app/src/app/components/header/header.component.scss +++ b/angular-primeng-app/src/app/components/header/header.component.scss @@ -64,59 +64,8 @@ p-toolbar { } } -p-dialog { - h3 { - font-size: 1.1rem; - font-weight: 400; - text-align: center; - } - - .logo-wrapper { - display: flex; - align-items: center; - justify-content: center; - - .logo { - width: 5rem; - height: 5rem; - margin: 1rem; - border-radius: 50%; - } - } - - .button { - display: flex; - align-items: center; - justify-content: center; - background-color: #2563eb; - color: #fff; - font-size: 1.1rem; - padding: 0.7rem 3rem; - margin: 2rem 1rem; - border: none; - border-radius: 3rem; - - img { - width: 1.5rem; - height: 1.5rem; - margin-right: 0.5rem; - } - } - - .link { - color: #2563eb; - font-size: 0.8.5rem; - margin: 0; - } - - p { - font-size: 0.8rem; - text-align: center; - } -} - .controls { display: flex; align-items: center; - gap: 10px; + gap: 0.5rem; } diff --git a/angular-primeng-app/src/app/components/header/header.component.ts b/angular-primeng-app/src/app/components/header/header.component.ts index 9e26cdc..4ecfb96 100644 --- a/angular-primeng-app/src/app/components/header/header.component.ts +++ b/angular-primeng-app/src/app/components/header/header.component.ts @@ -1,4 +1,4 @@ -import { Component, inject, OnInit } from "@angular/core"; +import { Component, inject, OnDestroy, OnInit } from "@angular/core"; import { FormsModule } from "@angular/forms"; import { ThemeService } from "../../services/theme.service"; import { BlogService } from "../../services/blog.service"; @@ -6,12 +6,15 @@ import { AsyncPipe, KeyValuePipe } from "@angular/common"; import { RouterLink } from "@angular/router"; import { BlogInfo, BlogLinks } from "../../models/blog-info"; import { SeriesList } from "../../models/post"; +import { SearchDialogComponent } from "../../partials/search-dialog/search-dialog.component"; +import { Subscription } from "rxjs"; import { ToolbarModule } from "primeng/toolbar"; import { ButtonModule } from "primeng/button"; import { InputSwitchModule } from "primeng/inputswitch"; import { DialogModule } from "primeng/dialog"; -import { SearchDialogComponent } from "../../partials/search-dialog/search-dialog.component"; +import { SettingsDialogComponent } from "../../partials/settings-dialog/settings-dialog.component"; +import { FollowDialogComponent } from "../../partials/follow-dialog/follow-dialog.component"; @Component({ selector: "app-header", @@ -19,6 +22,8 @@ import { SearchDialogComponent } from "../../partials/search-dialog/search-dialo imports: [ AsyncPipe, SearchDialogComponent, + SettingsDialogComponent, + FollowDialogComponent, ButtonModule, FormsModule, InputSwitchModule, @@ -30,10 +35,12 @@ import { SearchDialogComponent } from "../../partials/search-dialog/search-dialo templateUrl: "./header.component.html", styleUrl: "./header.component.scss", }) -export class HeaderComponent implements OnInit { - blogInfo!: BlogInfo; +export class HeaderComponent implements OnInit, OnDestroy { + blogURL!: string; + blogInfo!: BlogInfo; blogId: string = ""; - blogName: string = ""; + blogName: string = ""; + blogImage: string = "/assets/images/anguhashblog-logo-purple-bgr.jpg"; blogSocialLinks!: BlogLinks; checked: boolean = true; selectedTheme: string = "dark"; @@ -42,19 +49,38 @@ export class HeaderComponent implements OnInit { themeService: ThemeService = inject(ThemeService); blogService: BlogService = inject(BlogService); + private querySubscription?: Subscription; + ngOnInit(): void { - this.blogService - .getBlogInfo() + this.blogURL = this.blogService.getBlogURL(); + this.querySubscription = this.blogService + .getBlogInfo(this.blogURL) .subscribe((data) => { this.blogInfo = data; - this.blogId = this.blogInfo.id; + this.blogId = this.blogInfo.id; this.blogName = this.blogInfo.title; + if (this.blogInfo.isTeam && this.blogInfo.favicon) { + this.blogImage = this.blogInfo.favicon; + } else { + this.blogImage = '/assets/images/anguhashblog-logo-purple-bgr.jpg' + } + if (!this.blogInfo.isTeam) { + this.blogService + .getAuthorInfo(this.blogURL) + .subscribe((data) => { + if (data.profilePicture) { + this.blogImage = data.profilePicture; + } else { + this.blogImage = '/assets/images/anguhashblog-logo-purple-bgr.jpg' + } + }); + } const { __typename, ...links } = data.links; this.blogSocialLinks = links; }); this.blogService - .getSeriesList() + .getSeriesList(this.blogURL) .subscribe((data) => { this.seriesList = data; }); @@ -65,7 +91,7 @@ export class HeaderComponent implements OnInit { this.themeService.setTheme(theme); } - showDialog() { - this.visible = true; + ngOnDestroy(): void { + this.querySubscription?.unsubscribe(); } } diff --git a/angular-primeng-app/src/app/components/post-details/post-details.component.html b/angular-primeng-app/src/app/components/post-details/post-details.component.html index 7c2ace4..791c0d3 100644 --- a/angular-primeng-app/src/app/components/post-details/post-details.component.html +++ b/angular-primeng-app/src/app/components/post-details/post-details.component.html @@ -30,7 +30,7 @@

{{ post.title }}

-
+
} diff --git a/angular-primeng-app/src/app/components/post-details/post-details.component.scss b/angular-primeng-app/src/app/components/post-details/post-details.component.scss index 170edea..617f99f 100644 --- a/angular-primeng-app/src/app/components/post-details/post-details.component.scss +++ b/angular-primeng-app/src/app/components/post-details/post-details.component.scss @@ -75,6 +75,11 @@ .content { font-size: 1rem; line-height: 1.5rem; + + iframe { + width: 100%; + height: calc(50vw * 0.5625); + } } } diff --git a/angular-primeng-app/src/app/components/post-details/post-details.component.ts b/angular-primeng-app/src/app/components/post-details/post-details.component.ts index 24ed9ef..dd420a7 100644 --- a/angular-primeng-app/src/app/components/post-details/post-details.component.ts +++ b/angular-primeng-app/src/app/components/post-details/post-details.component.ts @@ -1,10 +1,10 @@ import { Component, inject, Input, OnDestroy, OnInit } from "@angular/core"; import { BlogService } from "../../services/blog.service"; import { AsyncPipe, DatePipe } from "@angular/common"; -import { Post } from "../../models/post"; +import { Post, SeriesList } from "../../models/post"; import { Observable, Subscription } from "rxjs"; -import { ActivatedRoute, RouterLink } from "@angular/router"; -import { BlogInfo } from "../../models/blog-info"; +import { RouterLink } from "@angular/router"; +import { BlogInfo, BlogLinks } from "../../models/blog-info"; import { FormsModule } from "@angular/forms"; import { SidenavComponent } from "../sidenav/sidenav.component"; import { SearchDialogComponent } from "../../partials/search-dialog/search-dialog.component"; @@ -15,7 +15,8 @@ import { TagModule } from "primeng/tag"; import { ToolbarModule } from "primeng/toolbar"; import { ButtonModule } from "primeng/button"; import { InputSwitchModule } from "primeng/inputswitch"; -import { DomSanitizer, SafeHtml } from "@angular/platform-browser"; +import { SanitizerHtmlPipe } from "../../pipes/sanitizer-html.pipe"; +import { YoutubeVideoEmbedDirective } from "../../directives/youtube-video-embed.directive"; @Component({ selector: "app-post-details", @@ -23,9 +24,11 @@ import { DomSanitizer, SafeHtml } from "@angular/platform-browser"; imports: [ DatePipe, AsyncPipe, + SanitizerHtmlPipe, RouterLink, SidenavComponent, FooterComponent, + YoutubeVideoEmbedDirective, FormsModule, TagModule, ToolbarModule, @@ -36,36 +39,47 @@ import { DomSanitizer, SafeHtml } from "@angular/platform-browser"; templateUrl: "./post-details.component.html", styleUrl: "./post-details.component.scss", }) -export class PostDetailsComponent implements OnInit { - post$!: Observable; - blogInfo!: BlogInfo; - blogId: string = ""; - blogName: string = ""; +export class PostDetailsComponent implements OnInit, OnDestroy { checked: boolean = true; selectedTheme: string = "dark"; + blogURL!: string; + blogInfo!: BlogInfo; + blogId: string = ""; + blogName: string = ""; + blogSocialLinks!: BlogLinks; + seriesList!: SeriesList[]; + post$!: Observable; themeService: ThemeService = inject(ThemeService); - private sanitizer: DomSanitizer = inject(DomSanitizer); private blogService: BlogService = inject(BlogService); + private querySubscription?: Subscription; - @Input({ required: true }) - set slug(slug: string) { - this.post$ = this.blogService.getSinglePost(slug); - } - - ngOnInit(): void { - this.blogService.getBlogInfo().subscribe((data) => { - this.blogInfo = data; - this.blogId = this.blogInfo.id; - this.blogName = this.blogInfo.title; - }); - } + @Input({ required: true }) slug!: string; - sanitizeHtml(html: string): SafeHtml { - return this.sanitizer.bypassSecurityTrustHtml(html); + ngOnInit(): void { + this.blogURL = this.blogService.getBlogURL(); + this.querySubscription = this.blogService + .getBlogInfo(this.blogURL) + .subscribe((data) => { + this.blogInfo = data; + this.blogId = this.blogInfo.id; + this.blogName = this.blogInfo.title; + const { __typename, ...links } = data.links; + this.blogSocialLinks = links; + }); + this.post$ = this.blogService.getSinglePost(this.blogURL,this.slug); + this.querySubscription = this.blogService + .getSeriesList(this.blogURL) + .subscribe((data) => { + this.seriesList = data; + }); } onThemeChange(theme: string): void { this.selectedTheme = theme; this.themeService.setTheme(theme); } + + ngOnDestroy(): void { + this.querySubscription?.unsubscribe(); + } } diff --git a/angular-primeng-app/src/app/components/posts/posts.component.html b/angular-primeng-app/src/app/components/posts/posts.component.html index 78aaee5..93c7649 100644 --- a/angular-primeng-app/src/app/components/posts/posts.component.html +++ b/angular-primeng-app/src/app/components/posts/posts.component.html @@ -1,11 +1,13 @@
@for (post of posts$ | async; track post.id) { - - - - - + + + + + + + }
diff --git a/angular-primeng-app/src/app/components/posts/posts.component.scss b/angular-primeng-app/src/app/components/posts/posts.component.scss index 34f6f64..033efd7 100644 --- a/angular-primeng-app/src/app/components/posts/posts.component.scss +++ b/angular-primeng-app/src/app/components/posts/posts.component.scss @@ -8,6 +8,10 @@ flex-wrap: wrap; justify-content: center; margin: 2rem 1rem; + + .post-card { + display: flex; + } } } diff --git a/angular-primeng-app/src/app/components/posts/posts.component.ts b/angular-primeng-app/src/app/components/posts/posts.component.ts index d61113c..3454d3a 100644 --- a/angular-primeng-app/src/app/components/posts/posts.component.ts +++ b/angular-primeng-app/src/app/components/posts/posts.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, inject } from '@angular/core'; import { BlogService } from '../../services/blog.service'; -import { Router } from '@angular/router'; +import { RouterLink } from '@angular/router'; import { Observable } from 'rxjs'; import { AsyncPipe } from '@angular/common'; import { CardModule } from 'primeng/card'; @@ -9,20 +9,17 @@ import { Post } from '../../models/post'; @Component({ selector: 'app-posts', standalone: true, - imports: [ AsyncPipe, CardModule], + imports: [AsyncPipe, RouterLink, CardModule], templateUrl: './posts.component.html', styleUrl: './posts.component.scss' }) export class PostsComponent implements OnInit { + blogURL!: string; posts$!: Observable; - private router = inject(Router); private blogService = inject(BlogService); - ngOnInit() { - this.posts$ = this.blogService.getPosts(); - } - - navigateToPost(slug: string) { - this.router.navigate(['/post', slug]); - } + ngOnInit() { + this.blogURL = this.blogService.getBlogURL(); + this.posts$ = this.blogService.getPosts(this.blogURL); + } } diff --git a/angular-primeng-app/src/app/components/series/series.component.html b/angular-primeng-app/src/app/components/series/series.component.html index fdd1a99..56f4420 100644 --- a/angular-primeng-app/src/app/components/series/series.component.html +++ b/angular-primeng-app/src/app/components/series/series.component.html @@ -1,11 +1,13 @@
@for (post of postsInSeries$ | async; track post.id) { - - - - - + + + + + + + }
diff --git a/angular-primeng-app/src/app/components/series/series.component.scss b/angular-primeng-app/src/app/components/series/series.component.scss index d365ed5..e3bcaae 100644 --- a/angular-primeng-app/src/app/components/series/series.component.scss +++ b/angular-primeng-app/src/app/components/series/series.component.scss @@ -8,5 +8,9 @@ flex-wrap: wrap; justify-content: center; margin: 2rem 1rem; + + .post-card { + display: flex; + } } } diff --git a/angular-primeng-app/src/app/components/series/series.component.ts b/angular-primeng-app/src/app/components/series/series.component.ts index 07d039f..ba43733 100644 --- a/angular-primeng-app/src/app/components/series/series.component.ts +++ b/angular-primeng-app/src/app/components/series/series.component.ts @@ -1,5 +1,5 @@ import { Component, inject } from '@angular/core'; -import { ActivatedRoute, Params, Router, RouterLink } from '@angular/router'; +import { ActivatedRoute, Params, RouterLink } from '@angular/router'; import { Observable, switchMap } from 'rxjs'; import { Post } from '../../models/post'; import { AsyncPipe } from "@angular/common"; @@ -14,22 +14,19 @@ import { CardModule } from 'primeng/card'; styleUrl: './series.component.scss' }) export class SeriesComponent { + blogURL!: string; slug: string = ""; postsInSeries$!: Observable; blogService: BlogService = inject(BlogService); - private router = inject(Router); route: ActivatedRoute = inject(ActivatedRoute); ngOnInit(): void { + this.blogURL = this.blogService.getBlogURL(); this.postsInSeries$ = this.route.params.pipe( switchMap((params: Params) => { this.slug = params["slug"]; - return this.blogService.getPostsInSeries(this.slug); + return this.blogService.getPostsInSeries(this.blogURL, this.slug); }) ); } - - navigateToPost(slug: string) { - this.router.navigate(['/post', slug]); - } } diff --git a/angular-primeng-app/src/app/components/sidenav/sidenav.component.ts b/angular-primeng-app/src/app/components/sidenav/sidenav.component.ts index 0f9540a..a29ebcc 100644 --- a/angular-primeng-app/src/app/components/sidenav/sidenav.component.ts +++ b/angular-primeng-app/src/app/components/sidenav/sidenav.component.ts @@ -22,6 +22,7 @@ import { KeyValuePipe } from '@angular/common'; }) export class SidenavComponent implements OnInit, OnDestroy { sidebarVisible: boolean = false; + blogURL!: string; blogInfo!: BlogInfo; blogSocialLinks!: BlogLinks; seriesList!: SeriesList[]; @@ -29,15 +30,16 @@ export class SidenavComponent implements OnInit, OnDestroy { private querySubscription?: Subscription; ngOnInit(): void { + this.blogURL = this.blogService.getBlogURL(); this.querySubscription = this.blogService - .getBlogInfo() + .getBlogInfo(this.blogURL) .subscribe((data) => { this.blogInfo = data; const { __typename, ...links } = data.links; this.blogSocialLinks = links; }); this.querySubscription = this.blogService - .getSeriesList() + .getSeriesList(this.blogURL) .subscribe((data) => { this.seriesList = data; }); diff --git a/angular-primeng-app/src/app/directives/youtube-video-embed.directive.spec.ts b/angular-primeng-app/src/app/directives/youtube-video-embed.directive.spec.ts new file mode 100644 index 0000000..6108a51 --- /dev/null +++ b/angular-primeng-app/src/app/directives/youtube-video-embed.directive.spec.ts @@ -0,0 +1,8 @@ +import { YoutubeVideoDirective } from './youtube-video-embed.directive'; + +describe('YoutubeVideoDirective', () => { + it('should create an instance', () => { + const directive = new YoutubeVideoDirective(); + expect(directive).toBeTruthy(); + }); +}); diff --git a/angular-primeng-app/src/app/directives/youtube-video-embed.directive.ts b/angular-primeng-app/src/app/directives/youtube-video-embed.directive.ts new file mode 100644 index 0000000..a5d9891 --- /dev/null +++ b/angular-primeng-app/src/app/directives/youtube-video-embed.directive.ts @@ -0,0 +1,45 @@ +import { AfterViewInit, Directive, Renderer2, ElementRef } from "@angular/core"; + +@Directive({ + selector: "[youtubeVideoEmbed]", + standalone: true, +}) +export class YoutubeVideoEmbedDirective implements AfterViewInit { + constructor(private el: ElementRef, private render2: Renderer2) {} + + ngAfterViewInit(): void { + const embedWrappers: HTMLDivElement[] = Array.from( + this.el.nativeElement.querySelectorAll(".embed-wrapper") + ); + embedWrappers.forEach((embedWrapper) => { + let anchorElement: HTMLAnchorElement | null = + embedWrapper.querySelector(".embed-card"); + if (anchorElement) { + let youtubeURL: string = anchorElement.href; + let videoID: string = ""; + if (youtubeURL) { + if (youtubeURL.includes("youtube.com/watch?v=")) { + videoID = youtubeURL.split("v=")[1]; + } else if (youtubeURL.includes("youtu.be/")) { + videoID = youtubeURL.split("youtu.be/")[1]; + } + } + embedWrapper.innerHTML = ''; + this._buildIframe(embedWrapper, videoID); + } + }); + } + + private _buildIframe(block: HTMLDivElement, videoID: string): HTMLIFrameElement { + const iframe: HTMLIFrameElement = this.render2.createElement("iframe"); + this.render2.setAttribute( + iframe, + "src", + `https://www.youtube.com/embed/${videoID}` + ); + this.render2.setAttribute(iframe, "frameborder", "0"); + this.render2.setAttribute(iframe, "allowfullscreen", "true"); + this.render2.appendChild(block, iframe); + return iframe; + } +} diff --git a/angular-primeng-app/src/app/graphql.operations.ts b/angular-primeng-app/src/app/graphql.operations.ts index 1408525..c3a3157 100644 --- a/angular-primeng-app/src/app/graphql.operations.ts +++ b/angular-primeng-app/src/app/graphql.operations.ts @@ -1,176 +1,171 @@ import { gql } from "apollo-angular"; -export const BLOG_HOST = "hashnode.anguhashblog.com"; - export const GET_BLOG_INFO = gql` -query Publication { - publication(host: "${BLOG_HOST}") { - id - title - isTeam - links { - twitter - instagram - github - website - hashnode - youtube - dailydev - linkedin - mastodon - } - followersCount - url - favicon - } -} + query Publication($host: String!) { + publication(host: $host) { + id + title + isTeam + links { + twitter + instagram + github + website + hashnode + youtube + dailydev + linkedin + mastodon + } + followersCount + url + favicon + } + } `; export const GET_AUTHOR_INFO = gql` -query Publication { - publication(host: "${BLOG_HOST}") { - author { - id - username - profilePicture - socialMediaLinks { - __typename - facebook - github - instagram - linkedin - stackoverflow - twitter - website - youtube - } - } - } -} + query Publication($host: String!) { + publication(host: $host) { + author { + id + username + profilePicture + socialMediaLinks { + __typename + facebook + github + instagram + linkedin + stackoverflow + twitter + website + youtube + } + } + } + } `; export const GET_POSTS = gql` -query Publication { - publication(host: "${BLOG_HOST}") { - id, - isTeam, - title, - posts(first: 10) { - edges { - node { - id, - slug, - coverImage { - url - } - title, - brief, - content { - html - } - } - } - } - } -} + query Publication($host: String!) { + publication(host: $host) { + id + isTeam + title + posts(first: 10) { + edges { + node { + id + slug + coverImage { + url + } + title + brief + content { + html + } + } + } + } + } + } `; export const GET_SERIES_LIST = gql` -query Publication { - publication(host: "${BLOG_HOST}") { - id, - title, - seriesList(first:10) { - edges { - node { - id, - name, - slug, - } - } - } - } -} + query Publication($host: String!) { + publication(host: $host) { + id + title + seriesList(first: 10) { + edges { + node { + id + name + slug + } + } + } + } + } `; export const GET_POSTS_IN_SERIES = gql` -query Publication ($slug: String!) { - publication(host: "${BLOG_HOST}") { - id, - isTeam , - title, - series(slug: $slug) { - posts(first: 10) { - edges { - node { - id, - title, - slug, - coverImage { - url - } - } - } - } - } - } -} + query Publication($host: String!, $slug: String!) { + publication(host: $host) { + id + isTeam + title + series(slug: $slug) { + posts(first: 10) { + edges { + node { + id + title + slug + coverImage { + url + } + } + } + } + } + } + } `; export const GET_SINGLE_POST = gql` -query SinglePost($slug: String!) { - publication(host: "${BLOG_HOST}") { - post(slug: $slug) { - id, - slug, - title, - readTimeInMinutes, - tags { - name - }, - author { - name, - profilePicture, - socialMediaLinks { - twitter - youtube - } - } - coverImage { - url - }, - content { - html - }, - publishedAt, - } - } -} + query SinglePost($host: String!, $slug: String!) { + publication(host: $host) { + id + post(slug: $slug) { + id + slug + title + readTimeInMinutes + tags { + name + } + author { + name + profilePicture + } + coverImage { + url + } + content { + html + } + publishedAt + } + } + } `; export const SEARCH_POSTS = gql` -query SearchPostsOfPublicationFilter($publicationId: ObjectId!, $query: String!) { - searchPostsOfPublication( - first: 5, - filter: { - publicationId: $publicationId, - query: $query - } - ) { - edges { - node { - id, - slug, - coverImage { - url - }, - author { - name - }, - publishedAt, - title - } - } - } -} + query SearchPostsOfPublicationFilter( + $publicationId: ObjectId! + $query: String! + ) { + searchPostsOfPublication( + first: 5 + filter: { publicationId: $publicationId, query: $query } + ) { + edges { + node { + id + slug + coverImage { + url + } + author { + name + } + publishedAt + title + } + } + } + } `; diff --git a/angular-primeng-app/src/app/partials/follow-dialog/follow-dialog.component.html b/angular-primeng-app/src/app/partials/follow-dialog/follow-dialog.component.html new file mode 100644 index 0000000..6491a6a --- /dev/null +++ b/angular-primeng-app/src/app/partials/follow-dialog/follow-dialog.component.html @@ -0,0 +1,10 @@ + + + +
+ +
+

Hey, 👋 sign up or sign in to interact.

+ Sign inSign in with Hashnode +

This blog is powered by Hashnode. To interact with the content on this blog, please log in through Hashnode.

+
diff --git a/angular-primeng-app/src/app/partials/follow-dialog/follow-dialog.component.scss b/angular-primeng-app/src/app/partials/follow-dialog/follow-dialog.component.scss new file mode 100644 index 0000000..654cd29 --- /dev/null +++ b/angular-primeng-app/src/app/partials/follow-dialog/follow-dialog.component.scss @@ -0,0 +1,50 @@ +p-dialog { + h3 { + font-size: 1.1rem; + font-weight: 400; + text-align: center; + } + + .logo-wrapper { + display: flex; + align-items: center; + justify-content: center; + + .logo { + width: 5rem; + height: 5rem; + margin: 1rem; + border-radius: 50%; + } + } + + .button { + display: flex; + align-items: center; + justify-content: center; + background-color: #2563eb; + color: #fff; + font-size: 1.1rem; + padding: 0.7rem 3rem; + margin: 2rem 1rem; + border: none; + border-radius: 3rem; + + img { + width: 1.5rem; + height: 1.5rem; + margin-right: 0.5rem; + } + } + + .link { + color: #2563eb; + font-size: 0.8.5rem; + margin: 0; + } + + p { + font-size: 0.8rem; + text-align: center; + } +} diff --git a/angular-primeng-app/src/app/partials/follow-dialog/follow-dialog.component.spec.ts b/angular-primeng-app/src/app/partials/follow-dialog/follow-dialog.component.spec.ts new file mode 100644 index 0000000..29b29d1 --- /dev/null +++ b/angular-primeng-app/src/app/partials/follow-dialog/follow-dialog.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FollowDialogComponent } from './follow-dialog.component'; + +describe('FollowDialogComponent', () => { + let component: FollowDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FollowDialogComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(FollowDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/angular-primeng-app/src/app/partials/follow-dialog/follow-dialog.component.ts b/angular-primeng-app/src/app/partials/follow-dialog/follow-dialog.component.ts new file mode 100644 index 0000000..e7eba2d --- /dev/null +++ b/angular-primeng-app/src/app/partials/follow-dialog/follow-dialog.component.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { ButtonModule } from 'primeng/button'; +import { DialogModule } from 'primeng/dialog'; + +@Component({ + selector: 'app-follow-dialog', + standalone: true, + imports: [DialogModule, ButtonModule], + templateUrl: './follow-dialog.component.html', + styleUrl: './follow-dialog.component.scss' +}) +export class FollowDialogComponent { + visible: boolean = false; + + showDialog() { + this.visible = true; + } +} diff --git a/angular-primeng-app/src/app/partials/search-dialog/search-dialog.component.html b/angular-primeng-app/src/app/partials/search-dialog/search-dialog.component.html index ce82b10..4e997f5 100644 --- a/angular-primeng-app/src/app/partials/search-dialog/search-dialog.component.html +++ b/angular-primeng-app/src/app/partials/search-dialog/search-dialog.component.html @@ -1,36 +1,36 @@ - + + [style]="{ width: '60%', marginTop: '150px', maxHeight: '70%' }" [draggable]="false" [dismissableMask]="true" + header="Search posts"> @if (queryFormControl.value) { - + } + [formControl]="queryFormControl"> @if (queryFormControl.value && posts.length !== 0) { - @for (post of posts; track post.id) { -
-
-

{{ post.title }}

- - -
+ @for (post of posts; track post.id) { +
+
+

{{ post.title }}

+ + +
- -
- } + +
+ } } @else { - @if (queryFormControl.value) { -

No matching articles found. Try another search

- } @else { -

Search articles from this blog

- } + @if (queryFormControl.value) { +

No matching articles found. Try another search

+ } @else { +

Search articles from this blog

+ } }
diff --git a/angular-primeng-app/src/app/partials/search-dialog/search-dialog.component.ts b/angular-primeng-app/src/app/partials/search-dialog/search-dialog.component.ts index 2ab0cd0..20e8f36 100644 --- a/angular-primeng-app/src/app/partials/search-dialog/search-dialog.component.ts +++ b/angular-primeng-app/src/app/partials/search-dialog/search-dialog.component.ts @@ -1,4 +1,4 @@ -import { Component, inject, Input, OnInit } from '@angular/core'; +import { Component, inject, Input, OnInit } from "@angular/core"; import { DialogModule } from "primeng/dialog"; import { InputTextModule } from "primeng/inputtext"; import { ButtonModule } from "primeng/button"; @@ -9,51 +9,50 @@ import { DatePipe } from "@angular/common"; import { Router } from "@angular/router"; @Component({ - selector: 'app-search-dialog', - standalone: true, - imports: [ - DialogModule, - InputTextModule, - ButtonModule, - FormsModule, - DatePipe, - ReactiveFormsModule - ], - templateUrl: './search-dialog.component.html', - styleUrl: './search-dialog.component.scss' + selector: "app-search-dialog", + standalone: true, + imports: [ + DialogModule, + InputTextModule, + ButtonModule, + FormsModule, + DatePipe, + ReactiveFormsModule, + ], + templateUrl: "./search-dialog.component.html", + styleUrl: "./search-dialog.component.scss", }) export class SearchDialogComponent implements OnInit { - @Input({required: true}) blogId!: string; - - visible = false; - posts: Post[] = []; - queryFormControl = new FormControl(''); - blogService: BlogService = inject(BlogService); - router: Router = inject(Router); - - - ngOnInit() { - this.queryFormControl.valueChanges.subscribe(query => this.searchPosts(query)); - } - - showDialog() { - this.visible = true; - } - - searchPosts(query: string | null) { - this.blogService.searchPosts(this.blogId, query) - .subscribe(response => { - this.posts = response; - console.log(this.posts); - }); - } - - navigateToPost(slug: string) { - this.visible = false; - this.router.navigate(['/post', slug]); - } - - clearQuery() { - this.queryFormControl.reset(); - } + @Input({ required: true }) blogId!: string; + + visible = false; + posts: Post[] = []; + queryFormControl = new FormControl(""); + blogService: BlogService = inject(BlogService); + router: Router = inject(Router); + + ngOnInit() { + this.queryFormControl.valueChanges.subscribe((query) => + this.searchPosts(query) + ); + } + + showDialog() { + this.visible = true; + } + + searchPosts(query: string | null) { + this.blogService.searchPosts(this.blogId, query).subscribe((response) => { + this.posts = response; + }); + } + + navigateToPost(slug: string) { + this.visible = false; + this.router.navigate(["/post", slug]); + } + + clearQuery() { + this.queryFormControl.reset(); + } } diff --git a/angular-primeng-app/src/app/partials/settings-dialog/settings-dialog.component.html b/angular-primeng-app/src/app/partials/settings-dialog/settings-dialog.component.html new file mode 100644 index 0000000..d54a69a --- /dev/null +++ b/angular-primeng-app/src/app/partials/settings-dialog/settings-dialog.component.html @@ -0,0 +1,33 @@ + + + + @if (blogURLChanged) { +
+

Blog URL changed and set in local storage

+

Reload the page to see your content loading

+

When resetting you may need to click on

+

the logo image to load the content again

+
+ +
+
+ } @else { +
+

Try with your Blog

+

try AnguHashBlog

+ with another Hashnode blog + + @if (newBlogURL) { + + } + + +
+
+
+ +
+
+ } +
diff --git a/angular-primeng-app/src/app/partials/settings-dialog/settings-dialog.component.scss b/angular-primeng-app/src/app/partials/settings-dialog/settings-dialog.component.scss new file mode 100644 index 0000000..67b18ea --- /dev/null +++ b/angular-primeng-app/src/app/partials/settings-dialog/settings-dialog.component.scss @@ -0,0 +1,37 @@ +p-dialog { + + .dialog-content, + .dialog-actions { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + h3 { + font-size: 1.2rem; + font-weight: 400; + text-align: center; + margin: 0; + } + + p-button { + margin: 0.5rem 0; + } + + p { + font-size: 1rem; + text-align: center; + margin: 0.7rem 0; + } + + small { + text-align: center; + } + + input { + margin : 1rem 0; + } + } + +} + diff --git a/angular-primeng-app/src/app/partials/settings-dialog/settings-dialog.component.spec.ts b/angular-primeng-app/src/app/partials/settings-dialog/settings-dialog.component.spec.ts new file mode 100644 index 0000000..a93b622 --- /dev/null +++ b/angular-primeng-app/src/app/partials/settings-dialog/settings-dialog.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SettingsDialogComponent } from './settings-dialog.component'; + +describe('ettingsDialogComponent', () => { + let component: SettingsDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SettingsDialogComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SettingsDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/angular-primeng-app/src/app/partials/settings-dialog/settings-dialog.component.ts b/angular-primeng-app/src/app/partials/settings-dialog/settings-dialog.component.ts new file mode 100644 index 0000000..739d4b4 --- /dev/null +++ b/angular-primeng-app/src/app/partials/settings-dialog/settings-dialog.component.ts @@ -0,0 +1,57 @@ +import { Component, inject, OnInit } from "@angular/core"; +import { BlogService } from "../../services/blog.service"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; + +import { DialogModule } from "primeng/dialog"; +import { InputTextModule } from "primeng/inputtext"; +import { ButtonModule } from "primeng/button"; + +@Component({ + selector: "app-settings-dialog", + standalone: true, + imports: [ + DialogModule, + InputTextModule, + ButtonModule, + FormsModule, + ReactiveFormsModule, + ], + templateUrl: "./settings-dialog.component.html", + styleUrl: "./settings-dialog.component.scss", +}) +export class SettingsDialogComponent implements OnInit { + visible = false; + blogURL: string = "hashnode.anguhashblog.com"; + newBlogURL: string = ""; + blogURLChanged: boolean = false; + blogService: BlogService = inject(BlogService); + + ngOnInit() { + this.blogURL = this.blogService.getBlogURL(); + if (this.blogURL === "hashnode.anguhashblog.com") { + this.blogURLChanged = false; + } else { + this.blogURLChanged = true; + } + } + + changeBlogURL(): void { + this.blogService.setBlogURL(this.newBlogURL); + this.blogURL = this.blogService.getBlogURL(); + if (this.blogURL === "hashnode.anguhashblog.com") { + this.blogURLChanged = false; + } else { + this.blogURLChanged = true; + } + } + + resetBlogURL(): void { + this.blogService.resetBlogURL(); + this.blogURL = this.blogService.getBlogURL(); + this.blogURLChanged = false; + } + + showDialog() { + this.visible = true; + } +} diff --git a/angular-primeng-app/src/app/pipes/sanitizer-html.pipe.spec.ts b/angular-primeng-app/src/app/pipes/sanitizer-html.pipe.spec.ts new file mode 100644 index 0000000..5cb2008 --- /dev/null +++ b/angular-primeng-app/src/app/pipes/sanitizer-html.pipe.spec.ts @@ -0,0 +1,8 @@ +import { SanitizerHtmlPipe } from './sanitizer-html.pipe'; + +describe('SanitizerHtmlPipe', () => { + it('create an instance', () => { + const pipe = new SanitizerHtmlPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/angular-primeng-app/src/app/pipes/sanitizer-html.pipe.ts b/angular-primeng-app/src/app/pipes/sanitizer-html.pipe.ts new file mode 100644 index 0000000..ba4e412 --- /dev/null +++ b/angular-primeng-app/src/app/pipes/sanitizer-html.pipe.ts @@ -0,0 +1,14 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; + +@Pipe({ + name: 'sanitizerHtml', + standalone: true +}) +export class SanitizerHtmlPipe implements PipeTransform { + constructor(private domSanitizer: DomSanitizer) {} + + transform(value: string): SafeHtml { + return this.domSanitizer.bypassSecurityTrustHtml(value); + } +} diff --git a/angular-primeng-app/src/app/services/blog.service.ts b/angular-primeng-app/src/app/services/blog.service.ts index 8e4443e..b74c943 100644 --- a/angular-primeng-app/src/app/services/blog.service.ts +++ b/angular-primeng-app/src/app/services/blog.service.ts @@ -1,85 +1,155 @@ -import { Injectable } from '@angular/core'; +import { Injectable, Inject, PLATFORM_ID } from "@angular/core"; import { Apollo } from "apollo-angular"; -import { Observable, map, of } from 'rxjs'; -import { GET_AUTHOR_INFO, GET_BLOG_INFO, GET_POSTS, GET_POSTS_IN_SERIES, GET_SERIES_LIST, GET_SINGLE_POST, SEARCH_POSTS } from '../graphql.operations'; -import { Author, Post, SeriesList } from '../models/post'; -import { BlogInfo } from '../models/blog-info'; +import { Observable, map, of } from "rxjs"; +import { + GET_AUTHOR_INFO, + GET_BLOG_INFO, + GET_POSTS, + GET_POSTS_IN_SERIES, + GET_SERIES_LIST, + GET_SINGLE_POST, + SEARCH_POSTS, +} from "../graphql.operations"; +import { Author, Post, SeriesList } from "../models/post"; +import { BlogInfo } from "../models/blog-info"; +import { isPlatformBrowser } from "@angular/common"; @Injectable({ - providedIn: 'root' + providedIn: "root", }) export class BlogService { + blogURL: string = "hashnode.anguhashblog.com"; + private readonly localStorageKey = "userBlogURL"; - constructor(private apollo: Apollo) { } + constructor( + private apollo: Apollo, + @Inject(PLATFORM_ID) private platformId: Object + ) {} - getBlogInfo(): Observable { - return this.apollo - .watchQuery({ - query: GET_BLOG_INFO, - }) - .valueChanges.pipe(map(({ data }) => data.publication)); - } + getBlogURL(): string { + if (isPlatformBrowser(this.platformId)) { + return ( + localStorage.getItem(this.localStorageKey) || + "hashnode.anguhashblog.com" + ); + } + return "hashnode.anguhashblog.com"; + } - getAuthorInfo(): Observable { - return this.apollo - .watchQuery({ - query: GET_AUTHOR_INFO, - }) - .valueChanges.pipe(map(({ data }) => data.publication.author)); - } + setBlogURL(newBlogURL: string): void { + if (isPlatformBrowser(this.platformId)) { + localStorage.setItem(this.localStorageKey, newBlogURL); + } + this.blogURL = newBlogURL; + } - getPosts(): Observable { - return this.apollo - .watchQuery({ - query: GET_POSTS, - }) - .valueChanges.pipe(map(({ data }) => data.publication.posts.edges.map((edge: { node: any; }) => edge.node))); - } + resetBlogURL(): void { + localStorage.removeItem(this.localStorageKey); + this.blogURL = "hashnode.anguhashblog.com"; + } - getSeriesList(): Observable { - return this.apollo - .watchQuery({ - query: GET_SERIES_LIST, - }) - .valueChanges.pipe(map(({ data }) => data.publication.seriesList.edges.map((edge: { node: any; }) => edge.node))); - } + getBlogInfo(host: string): Observable { + return this.apollo + .watchQuery({ + query: GET_BLOG_INFO, + variables: { + host: host, + }, + }) + .valueChanges.pipe(map(({ data }) => data.publication)); + } - getPostsInSeries(slug: string): Observable { - return this.apollo - .watchQuery({ - query: GET_POSTS_IN_SERIES, - variables: { - slug: slug, - }, - }) - .valueChanges.pipe(map(({ data }) => data.publication.series.posts.edges.map((edge: { node: any; }) => edge.node))); - } + getAuthorInfo(host: string): Observable { + return this.apollo + .watchQuery({ + query: GET_AUTHOR_INFO, + variables: { + host: host, + }, + }) + .valueChanges.pipe(map(({ data }) => data.publication.author)); + } + getPosts(host: string): Observable { + return this.apollo + .watchQuery({ + query: GET_POSTS, + variables: { + host: host, + }, + }) + .valueChanges.pipe( + map(({ data }) => + data.publication.posts.edges.map((edge: { node: any }) => edge.node) + ) + ); + } - getSinglePost(slug: string): Observable{ - return this.apollo - .watchQuery({ - query: GET_SINGLE_POST, - variables: { - slug: slug, - }, - }) - .valueChanges.pipe(map(({ data }) => data.publication.post)); - } + getSeriesList(host: string): Observable { + return this.apollo + .watchQuery({ + query: GET_SERIES_LIST, + variables: { + host: host, + }, + }) + .valueChanges.pipe( + map(({ data }) => + data.publication.seriesList.edges.map( + (edge: { node: any }) => edge.node + ) + ) + ); + } - searchPosts(blogId: string, query: string | null): Observable { - if (query === null || query.length === 0) { - return of([]); - } + getPostsInSeries(host: string, slug: string): Observable { + return this.apollo + .watchQuery({ + query: GET_POSTS_IN_SERIES, + variables: { + host: host, + slug: slug, + }, + }) + .valueChanges.pipe( + map(({ data }) => + data.publication.series.posts.edges.map( + (edge: { node: any }) => edge.node + ) + ) + ); + } - return this.apollo - .watchQuery({ - query: SEARCH_POSTS, - variables: { + getSinglePost(host: string, slug: string): Observable { + return this.apollo + .watchQuery({ + query: GET_SINGLE_POST, + variables: { + host: host, + slug: slug, + }, + }) + .valueChanges.pipe(map(({ data }) => data.publication.post)); + } + + searchPosts(blogId: string, query: string | null): Observable { + if (query === null || query.length === 0) { + return of([]); + } + return this.apollo + .watchQuery({ + query: SEARCH_POSTS, + variables: { publicationId: blogId, - query: query - } - }) - .valueChanges.pipe(map(({ data }) => data.searchPostsOfPublication.edges.map((edge: { node: any; }) => edge.node))); - } + query: query, + }, + }) + .valueChanges.pipe( + map(({ data }) => + data.searchPostsOfPublication.edges.map( + (edge: { node: any }) => edge.node + ) + ) + ); + } } diff --git a/angular-primeng-app/src/styles.scss b/angular-primeng-app/src/styles.scss index f51d21c..41a33d8 100644 --- a/angular-primeng-app/src/styles.scss +++ b/angular-primeng-app/src/styles.scss @@ -21,7 +21,7 @@ a { } .p-card-title { - font-size: 1.2rem; + font-size: 1.1rem; line-height: 1.7rem; font-weight: 500; } @@ -48,17 +48,17 @@ article { margin: 0 auto; } } - + pre { line-height: 2.2rem; padding: 1rem 1.5rem 1rem; border-radius: 0.3rem; position: relative; - + .hljs-keyword { color: #f92672; } - + .hljs-built_in { color: #e6db74; }