Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions angular-primeng-app/src/app/components/posts/posts.component.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
<div class="posts-view">
<div class="posts-view" infiniteScroll [isActiveInfiniteScroll]="isActiveInfiniteScroll" (scrolled)="loadMorePosts()">
<div class="cards-wrapper grid">
@for (post of posts$ | async; track post.id) {
<a [routerLink]="['post', post.slug]">
<p-card class="post-card" header="{{ post.title }}">
<ng-template pTemplate="header">
<img class="card-image" [src]="post.coverImage.url" [alt]="post.title + ' image'" />
</ng-template>
</p-card>
</a>
@for (post of posts; track post) {
<a [routerLink]="['post', post.slug]">
<p-card class="post-card" header="{{ post.title }}">
<ng-template pTemplate="header">
<img class="card-image" [src]="post.coverImage.url" [alt]="post.title + ' image'"/>
</ng-template>
</p-card>
</a>
}
</div>

@if (paginationInfo.hasNextPage && !isHiddenLoadMore) {
<div class="load-more-posts">
<p-button (click)="loadMorePosts()">Load more</p-button>
</div>
}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
display: flex;
}
}

.load-more-posts {
display: flex;
justify-content: center;
margin-bottom: 2rem;
}
}


Expand Down
40 changes: 34 additions & 6 deletions angular-primeng-app/src/app/components/posts/posts.component.ts
Original file line number Diff line number Diff line change
@@ -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<Post[]>;
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);
});
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<div class="series-view">
<div class="series-view" infiniteScroll [isActiveInfiniteScroll]="isActiveInfiniteScroll"
(scrolled)="loadMorePostsFromSeries()">
<div class="cards-wrapper grid">
@for (post of postsInSeries$ | async; track post.id) {
@for (post of postsInSeries; track post) {
<a [routerLink]="['/post', post.slug]">
<p-card class="post-card" header="{{ post.title }}">
<ng-template pTemplate="header">
Expand All @@ -10,4 +11,10 @@
</a>
}
</div>

@if (paginationInfo.hasNextPage && !isHiddenLoadMore) {
<div class="load-more-posts">
<p-button (click)="loadMorePostsFromSeries()">Load more</p-button>
</div>
}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,10 @@
display: flex;
}
}

.load-more-posts {
display: flex;
justify-content: center;
margin-bottom: 2rem;
}
}
44 changes: 33 additions & 11 deletions angular-primeng-app/src/app/components/series/series.component.ts
Original file line number Diff line number Diff line change
@@ -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<Post[]>;
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);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { InfiniteScrollDirective } from './infinite-scroll.directive';

describe('InfiniteScrollDirective', () => {
it('should create an instance', () => {
const directive = new InfiniteScrollDirective();
expect(directive).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -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<boolean> = input(false);
@Input() infiniteScrollDistance: number = 20;

@Output() scrolled = new EventEmitter<void>;

private unsubscribe: Subject<void> = new Subject<void>();

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();
}
});
}
}
16 changes: 12 additions & 4 deletions angular-primeng-app/src/app/graphql.operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -68,6 +68,10 @@ export const GET_POSTS = gql`
}
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
Expand All @@ -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
Expand All @@ -109,6 +113,10 @@ export const GET_POSTS_IN_SERIES = gql`
}
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions angular-primeng-app/src/app/models/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,12 @@ export interface Content {
html: string;
}

export interface PostsPageInfo {
posts: Post[];
pagination: PageInfo;
}

export interface PageInfo {
hasNextPage: boolean;
endCursor: string;
}
Loading