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 93c7649..cb80b9d 100644
--- a/angular-primeng-app/src/app/components/posts/posts.component.html
+++ b/angular-primeng-app/src/app/components/posts/posts.component.html
@@ -1,13 +1,19 @@
-
+
- @for (post of posts$ | async; track post.id) {
-
-
-
-
-
-
-
+ @for (post of posts; track post) {
+
+
+
+
+
+
+
}
+
+ @if (paginationInfo.hasNextPage && !isHiddenLoadMore) {
+
+ }
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 033efd7..28ff44e 100644
--- a/angular-primeng-app/src/app/components/posts/posts.component.scss
+++ b/angular-primeng-app/src/app/components/posts/posts.component.scss
@@ -13,6 +13,12 @@
display: flex;
}
}
+
+ .load-more-posts {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 2rem;
+ }
}
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 3454d3a..097aeb2 100644
--- a/angular-primeng-app/src/app/components/posts/posts.component.ts
+++ b/angular-primeng-app/src/app/components/posts/posts.component.ts
@@ -1,25 +1,53 @@
-import { Component, OnInit, inject } from '@angular/core';
+import { Component, inject, OnInit } from '@angular/core';
import { BlogService } from '../../services/blog.service';
import { RouterLink } from '@angular/router';
-import { Observable } from 'rxjs';
import { AsyncPipe } from '@angular/common';
import { CardModule } from 'primeng/card';
-import { Post } from '../../models/post';
+import { PageInfo, Post } from '../../models/post';
+import { InfiniteScrollDirective } from "../../directives/infinite-scroll.directive";
+import { ButtonModule } from "primeng/button";
@Component({
selector: 'app-posts',
standalone: true,
- imports: [AsyncPipe, RouterLink, CardModule],
+ imports: [AsyncPipe, RouterLink, CardModule, InfiniteScrollDirective, ButtonModule],
templateUrl: './posts.component.html',
styleUrl: './posts.component.scss'
})
export class PostsComponent implements OnInit {
blogURL!: string;
- posts$!: Observable
;
+ posts!: Post[];
+ paginationInfo: PageInfo = { hasNextPage: true, endCursor: ''};
+ isHiddenLoadMore: boolean = true;
+ isActiveInfiniteScroll: boolean = false;
+
private blogService = inject(BlogService);
ngOnInit() {
this.blogURL = this.blogService.getBlogURL();
- this.posts$ = this.blogService.getPosts(this.blogURL);
+ this.loadPosts();
}
+
+ private loadPosts(): void {
+ this.blogService.getPosts(this.blogURL, this.paginationInfo.endCursor).subscribe(postsPageInfo => {
+ this.paginationInfo = postsPageInfo.pagination;
+ this.isHiddenLoadMore = !postsPageInfo.pagination.hasNextPage;
+ this.posts = postsPageInfo.posts;
+ });
+ }
+
+ loadMorePosts(): void {
+ if (!this.paginationInfo.hasNextPage) {
+ return;
+ }
+
+ this.isHiddenLoadMore = true;
+
+ this.blogService.getPosts(this.blogURL, this.paginationInfo.endCursor)
+ .subscribe(newPosts => {
+ this.isActiveInfiniteScroll = true;
+ this.paginationInfo = newPosts.pagination;
+ this.posts = this.posts.concat(newPosts.posts);
+ });
+ }
}
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 56f4420..3607db7 100644
--- a/angular-primeng-app/src/app/components/series/series.component.html
+++ b/angular-primeng-app/src/app/components/series/series.component.html
@@ -1,6 +1,7 @@
-
+
- @for (post of postsInSeries$ | async; track post.id) {
+ @for (post of postsInSeries; track post) {
@@ -10,4 +11,10 @@
}
+
+ @if (paginationInfo.hasNextPage && !isHiddenLoadMore) {
+
+ }
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 e3bcaae..49c2d71 100644
--- a/angular-primeng-app/src/app/components/series/series.component.scss
+++ b/angular-primeng-app/src/app/components/series/series.component.scss
@@ -13,4 +13,10 @@
display: flex;
}
}
+
+ .load-more-posts {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 2rem;
+ }
}
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 ba43733..137a460 100644
--- a/angular-primeng-app/src/app/components/series/series.component.ts
+++ b/angular-primeng-app/src/app/components/series/series.component.ts
@@ -1,32 +1,54 @@
import { Component, inject } from '@angular/core';
-import { ActivatedRoute, Params, RouterLink } from '@angular/router';
-import { Observable, switchMap } from 'rxjs';
-import { Post } from '../../models/post';
+import { ActivatedRoute, RouterLink } from '@angular/router';
+import { PageInfo, Post } from '../../models/post';
import { AsyncPipe } from "@angular/common";
import { BlogService } from '../../services/blog.service';
import { CardModule } from 'primeng/card';
+import { InfiniteScrollDirective } from "../../directives/infinite-scroll.directive";
+import { ButtonModule } from "primeng/button";
@Component({
selector: 'app-series',
standalone: true,
- imports: [RouterLink, AsyncPipe, CardModule],
+ imports: [RouterLink, AsyncPipe, CardModule, InfiniteScrollDirective, ButtonModule],
templateUrl: './series.component.html',
styleUrl: './series.component.scss'
})
export class SeriesComponent {
blogURL!: string;
slug: string = "";
- postsInSeries$!: Observable
;
+ postsInSeries: Post[] = [];
+ paginationInfo: PageInfo = { hasNextPage: true, endCursor: '' };
+ isHiddenLoadMore: boolean = true;
+ isActiveInfiniteScroll: boolean = false;
+
blogService: BlogService = inject(BlogService);
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.blogURL, this.slug);
- })
- );
+ this.route.params.subscribe(params => {
+ this.slug = params['slug'];
+ this.loadPostsInSeries();
+ })
}
+
+ private loadPostsInSeries():void{
+ this.blogService.getPostsInSeries(this.blogURL, this.slug).subscribe(seriesPageInfo => {
+ this.paginationInfo = seriesPageInfo.pagination;
+ this.isHiddenLoadMore = !seriesPageInfo.pagination.hasNextPage;
+ this.postsInSeries = seriesPageInfo.posts;
+ })
+ }
+
+ loadMorePostsFromSeries():void {
+ if (!this.paginationInfo.hasNextPage) return;
+ this.isHiddenLoadMore = true;
+ this.blogService.getPostsInSeries(this.blogURL, this.slug, this.paginationInfo.endCursor).pipe(
+ ).subscribe(newPosts => {
+ this.isActiveInfiniteScroll = true;
+ this.paginationInfo = newPosts.pagination;
+ this.postsInSeries = this.postsInSeries.concat(newPosts.posts);
+ });
+ }
}
diff --git a/angular-primeng-app/src/app/directives/infinite-scroll.directive.spec.ts b/angular-primeng-app/src/app/directives/infinite-scroll.directive.spec.ts
new file mode 100644
index 0000000..2721ef5
--- /dev/null
+++ b/angular-primeng-app/src/app/directives/infinite-scroll.directive.spec.ts
@@ -0,0 +1,8 @@
+import { InfiniteScrollDirective } from './infinite-scroll.directive';
+
+describe('InfiniteScrollDirective', () => {
+ it('should create an instance', () => {
+ const directive = new InfiniteScrollDirective();
+ expect(directive).toBeTruthy();
+ });
+});
diff --git a/angular-primeng-app/src/app/directives/infinite-scroll.directive.ts b/angular-primeng-app/src/app/directives/infinite-scroll.directive.ts
new file mode 100644
index 0000000..41cc01a
--- /dev/null
+++ b/angular-primeng-app/src/app/directives/infinite-scroll.directive.ts
@@ -0,0 +1,55 @@
+import { Directive, effect, EventEmitter, input, Input, InputSignal, OnDestroy, Output } from '@angular/core';
+import { fromEvent, Subject, takeUntil } from 'rxjs';
+
+@Directive({
+ selector: '[infiniteScroll]',
+ standalone: true
+})
+export class InfiniteScrollDirective implements OnDestroy {
+ isActiveInfiniteScroll: InputSignal = input(false);
+ @Input() infiniteScrollDistance: number = 20;
+
+ @Output() scrolled = new EventEmitter;
+
+ private unsubscribe: Subject = new Subject();
+
+ constructor() {
+ this._listenSignals();
+ }
+
+ ngOnDestroy(): void {
+ this.unsubscribe.next();
+ this.unsubscribe.complete();
+ }
+
+ private listenScrollWindow(): void {
+ let isActivePercentageBeforeEnd = false;
+ fromEvent(window, 'scroll').pipe(
+ takeUntil(this.unsubscribe))
+ .subscribe(() => {
+ let percentageBeforeEnd = this.calculatePositionScroll();
+ if (!isActivePercentageBeforeEnd && percentageBeforeEnd <= this.infiniteScrollDistance) {
+ isActivePercentageBeforeEnd = true;
+ this.scrolled.emit();
+ } else if (percentageBeforeEnd >= this.infiniteScrollDistance) {
+ isActivePercentageBeforeEnd = false;
+ }
+ });
+ }
+
+ private calculatePositionScroll(): number {
+ let scrollHeight = document.documentElement.scrollHeight;
+ let innerHeight = window.innerHeight;
+ let scrollY = window.scrollY || 0;
+ return (scrollHeight - scrollY - innerHeight) / scrollHeight * 100;
+ }
+
+ private _listenSignals(): void {
+ effect(() => {
+ this.unsubscribe.next();
+ if (this.isActiveInfiniteScroll()) {
+ this.listenScrollWindow();
+ }
+ });
+ }
+}
diff --git a/angular-primeng-app/src/app/graphql.operations.ts b/angular-primeng-app/src/app/graphql.operations.ts
index c3a3157..9b76f93 100644
--- a/angular-primeng-app/src/app/graphql.operations.ts
+++ b/angular-primeng-app/src/app/graphql.operations.ts
@@ -48,12 +48,12 @@ export const GET_AUTHOR_INFO = gql`
`;
export const GET_POSTS = gql`
- query Publication($host: String!) {
+ query Publication($host: String!, $after: String!) {
publication(host: $host) {
id
isTeam
title
- posts(first: 10) {
+ posts(first: 10, after: $after) {
edges {
node {
id
@@ -68,6 +68,10 @@ export const GET_POSTS = gql`
}
}
}
+ pageInfo {
+ endCursor
+ hasNextPage
+ }
}
}
}
@@ -92,13 +96,13 @@ export const GET_SERIES_LIST = gql`
`;
export const GET_POSTS_IN_SERIES = gql`
- query Publication($host: String!, $slug: String!) {
+ query Publication($host: String!, $slug: String!, $after: String!) {
publication(host: $host) {
id
isTeam
title
series(slug: $slug) {
- posts(first: 10) {
+ posts(first: 10, after: $after) {
edges {
node {
id
@@ -109,6 +113,10 @@ export const GET_POSTS_IN_SERIES = gql`
}
}
}
+ pageInfo {
+ endCursor
+ hasNextPage
+ }
}
}
}
diff --git a/angular-primeng-app/src/app/models/post.ts b/angular-primeng-app/src/app/models/post.ts
index 076f3d0..ea8b9b2 100644
--- a/angular-primeng-app/src/app/models/post.ts
+++ b/angular-primeng-app/src/app/models/post.ts
@@ -47,3 +47,12 @@ export interface Content {
html: string;
}
+export interface PostsPageInfo {
+ posts: Post[];
+ pagination: PageInfo;
+}
+
+export interface PageInfo {
+ hasNextPage: boolean;
+ endCursor: string;
+}
diff --git a/angular-primeng-app/src/app/services/blog.service.ts b/angular-primeng-app/src/app/services/blog.service.ts
index b74c943..ddb899e 100644
--- a/angular-primeng-app/src/app/services/blog.service.ts
+++ b/angular-primeng-app/src/app/services/blog.service.ts
@@ -10,7 +10,7 @@ import {
GET_SINGLE_POST,
SEARCH_POSTS,
} from "../graphql.operations";
-import { Author, Post, SeriesList } from "../models/post";
+import { Author, Post, PostsPageInfo, SeriesList } from "../models/post";
import { BlogInfo } from "../models/blog-info";
import { isPlatformBrowser } from "@angular/common";
@@ -70,19 +70,23 @@ export class BlogService {
.valueChanges.pipe(map(({ data }) => data.publication.author));
}
- getPosts(host: string): Observable {
+ getPosts(host: string, after: string): Observable {
return this.apollo
.watchQuery({
query: GET_POSTS,
variables: {
host: host,
+ after: after
},
})
.valueChanges.pipe(
- map(({ data }) =>
- data.publication.posts.edges.map((edge: { node: any }) => edge.node)
- )
- );
+ map(({ data }) => {
+ const { edges, pageInfo } = data.publication.posts;
+ return {
+ posts: edges.map((edge: { node: any }) => edge.node),
+ pagination: pageInfo
+ }
+ }));
}
getSeriesList(host: string): Observable {
@@ -102,23 +106,24 @@ export class BlogService {
);
}
- 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
- )
- )
- );
- }
+ getPostsInSeries(host: string, slug: string, after: string = ""): Observable {
+ return this.apollo
+ .watchQuery({
+ query: GET_POSTS_IN_SERIES,
+ variables: {
+ host: host,
+ slug: slug,
+ after: after
+ },
+ }).valueChanges.pipe(
+ map(({ data }) => {
+ const { edges, pageInfo } = data.publication.series.posts;
+ return {
+ posts: edges.map((edge: { node: any; }) => edge.node),
+ pagination: pageInfo
+ };
+ }));
+ }
getSinglePost(host: string, slug: string): Observable {
return this.apollo