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
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"editor.formatOnSave": true,
}
39 changes: 36 additions & 3 deletions src/app/features/post/apis/post.api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HttpClient } from "@angular/common/http";
import { Injectable, inject } from "@angular/core";
import { inject, Injectable } from "@angular/core";
import { API_SERVICE } from "@core/constants";
import { map, Observable } from "rxjs";
import { Post } from "../models";

@Injectable({ providedIn: "root" })
Expand All @@ -9,10 +10,42 @@ export class PostApi {
private readonly baseUrl = API_SERVICE.POST;

getAll({ limit }: { limit: number }) {
return this.http.get<Post[]>(`${this.baseUrl}?_limit=${limit}`);
const url = `${this.baseUrl}?_limit=${limit}`;

return this.http.get<Post[]>(url).pipe(
map((body) => {
return body.map((data) => {
return Post.create(data);
});
}),
);
}

getById(id: number) {
return this.http.get<Post>(`${this.baseUrl}/${id}`);
const url = `${this.baseUrl}/${id}`;

return this.http.get<Post>(url).pipe(
map((body) => {
return Post.create(body);
}),
);
}

addPost(post: Omit<Post, "id">): Observable<Post> {
const url = `${this.baseUrl}`;

return this.http.post<Post>(url, post);
}

updatePost(id: number, body: Partial<Post>): Observable<Post> {
const url = `${this.baseUrl}/${id}`;

return this.http.put<Post>(url, body);
}

deletePost(id: number): Observable<void> {
const url = `${this.baseUrl}/${id}`;

return this.http.delete<void>(url);
}
}
16 changes: 15 additions & 1 deletion src/app/features/post/components/post.component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { ChangeDetectionStrategy, Component, inject, input } from "@angular/core";
import { ChangeDetectionStrategy, Component, inject, input, output } from "@angular/core";
import { Router } from "@angular/router";
import { Post } from "../models";

export interface PostSubmitEvent {
type: "delete" | "update";
data: Post;
}

@Component({
standalone: true,
selector: "app-post",
Expand All @@ -17,6 +22,7 @@ import { Post } from "../models";
<button class="app-btn-primary" (click)="router.navigateByUrl('/home/post/' + p.id)">
Read more
</button>
<button class="app-btn-primary" (click)="submitDelete()">Delete</button>
</div>
</div>
</div>
Expand All @@ -28,4 +34,12 @@ import { Post } from "../models";
export class PostComponent {
router = inject(Router);
data = input.required<Post>();
submitEvent = output<PostSubmitEvent>();

submitDelete() {
this.submitEvent.emit({
type: "delete",
data: this.data(),
});
}
}
14 changes: 7 additions & 7 deletions src/app/features/post/models/post.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ export class Post {
title = "";
body = "";

static create(input?: any) {
const m = new Post();
static create(input?: Partial<Post>) {
const model = new Post();

m.userId = input?.userId ?? m.userId;
m.id = input?.id ?? m.id;
m.title = input?.title ?? m.title;
m.body = input?.body ?? m.body;
model.userId = input?.userId ?? model.userId;
model.id = input?.id ?? model.id;
model.title = input?.title ?? model.title;
model.body = input?.body ?? model.body;

return m;
return model;
}
}
2 changes: 1 addition & 1 deletion src/app/features/post/pages/post-detail.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ export class PostDetailComponent implements OnInit {
post$?: Observable<FetchState<Post>>;

ngOnInit() {
this.post$ = this.postService.getPostById(this.id()).pipe(toFetchState());
this.post$ = this.postService.getPostDetailById(this.id()).pipe(toFetchState());
}
}
43 changes: 28 additions & 15 deletions src/app/features/post/pages/post-listing.component.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,47 @@
import { AsyncPipe } from "@angular/common";
import { ChangeDetectionStrategy, Component, inject } from "@angular/core";
import { toFetchState } from "@core/utils";
import { ChangeDetectionStrategy, Component, inject, OnInit, Signal } from "@angular/core";
import { StateType } from "@shared/services/base-store.service";
import { PostComponent } from "../components";
import { PostSubmitEvent } from "../components/post.component";
import { Post } from "../models";
import { PostService } from "../services";

@Component({
selector: "app-post-list",
imports: [AsyncPipe, PostComponent],
imports: [PostComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
template: `
<div class="styled-box">
<div class="divider text-accent">Post Listing Page</div>
@let posts = posts$ | async;
@let data = posts?.data;
@if (data) {
@for (p of data; track $index) {
<app-post [data]="p" />
@let state = postState();
@if (state.error) {
<div class="text-[red]">Loading posts failed....</div>
} @else if (state.loading) {
<span class="loading loading-spinner loading-xl"></span>
} @else {
@for (p of state.data; track $index) {
<app-post [data]="p" (submitEvent)="handlePostItemEvent($event)" />
} @empty {
<div class="text-center">No post</div>
}
} @else if (posts?.error) {
<div class="text-[red]">Loading posts failed....</div>
} @else {
<span class="loading loading-spinner loading-xl"></span>
}
</div>
`,
})
export class PostListingComponent {
export class PostListingComponent implements OnInit {
private readonly postService = inject(PostService);
protected posts$ = this.postService.getAllPosts().pipe(toFetchState());
protected postState: Signal<StateType<Post>> = this.postService.postState;

ngOnInit() {
this.postService.fetchAllPosts();
}

protected handlePostItemEvent(event: PostSubmitEvent) {
switch (event.type) {
case "delete":
return this.postService.deletePostById(event.data.id, false);
case "update":
return this.postService.updatePost(event.data.id, event.data, false);
}
}
}
10 changes: 10 additions & 0 deletions src/app/features/post/services/post-store.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Injectable } from "@angular/core";
import { BaseStore } from "@shared/services/base-store.service";
import { Post } from "../models";

@Injectable({
providedIn: "root",
})
export class PostStore extends BaseStore<Post> {
// override methods...
}
112 changes: 84 additions & 28 deletions src/app/features/post/services/post.service.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,107 @@
import { inject, Injectable } from "@angular/core";
import { BehaviorSubject, map, of } from "rxjs";
import { catchError, finalize, throwError } from "rxjs";
import { PostApi } from "../apis";
import { Post } from "../models";
import { PostStore } from "./post-store.service";

@Injectable({
providedIn: "root",
})
export class PostService {
private readonly PostApi = inject(PostApi);
private _posts$ = new BehaviorSubject<{
data: Post[];
expired: number;
}>({ data: [], expired: 0 });

get posts() {
return this._posts$.value.data;
}
private readonly postApi = inject(PostApi);
private readonly postStore = inject(PostStore);

clearCache() {
this._posts$.next({ data: [], expired: 0 });
get postState() {
return this.postStore.state;
}

isExpired() {
const expired = Date.now() > this._posts$.value.expired;
const expired = Date.now() > this.postStore.state().expired;
if (expired) {
this.clearCache();
this.postStore.reset();
}
return expired;
}

getAllPosts() {
if (this.posts.length && !this.isExpired()) {
return of(this.posts);
fetchAllPosts() {
if (!this.isExpired()) {
console.log("data not expired");
return;
}

return this.PostApi.getAll({ limit: 100 }).pipe(
map((data) => {
this._posts$.next({
data,
expired: Date.now() + 3 * 60 * 1000, // expires in 3 minutes
});
return data;
}),
);
this.postStore.setLoading(true);
this.postApi
.getAll({ limit: 20 })
.pipe(
catchError((error) => {
console.log("fetch all post", error);
return throwError(() => error);
}),
finalize(() => {
this.postStore.setLoading(false);
}),
)
.subscribe((data) => {
const expired = Date.now() + 1 * 60 * 1000;
this.postStore.setData(data, expired);
});
}

getPostDetailById(id: number) {
return this.postApi.getById(id);
}

createPost(data: Post, loading = true) {
this.postStore.setLoading(loading);
this.postApi
.addPost(data)
.pipe(
catchError((error) => {
console.log("create-post", error);
return throwError(() => error);
}),
finalize(() => {
this.postStore.setLoading(false);
}),
)
.subscribe((post) => {
this.postStore.addNewData(post);
});
}

updatePost(id: number, data: Partial<Post>, loading = true) {
this.postStore.setLoading(loading);
this.postApi
.updatePost(id, data)
.pipe(
catchError((error) => {
console.log("update-post", error);
return throwError(() => error);
}),
finalize(() => {
this.postStore.setLoading(false);
}),
)
.subscribe((post) => {
this.postStore.updateDataById(id, post);
});
}

getPostById(id: number) {
return this.PostApi.getById(id);
deletePostById(id: number, loading = true) {
this.postStore.setLoading(loading);
this.postApi
.deletePost(id)
.pipe(
catchError((error) => {
console.log("delete-post", error);
return throwError(() => error);
}),
finalize(() => {
this.postStore.setLoading(false);
}),
)
.subscribe(() => {
this.postStore.deleteDataById(id);
});
}
}
Loading