Skip to content

Commit e2ec259

Browse files
authored
FE - Implement user profiles and feeds (#17)
* FE - Implement user profiles and feeds
1 parent f1c9a84 commit e2ec259

File tree

13 files changed

+331
-21
lines changed

13 files changed

+331
-21
lines changed

frontend/src/app/app-routing.module.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import { HomeComponent } from "./pages/home/home.component";
44
import { LoginComponent } from "./pages/login/login.component";
55
import { RegisterComponent } from "./pages/register/register.component";
66
import { UserSettingsComponent } from "./pages/user-settings/user-settings.component";
7-
import {EditorComponent} from "./pages/editor/editor.component";
7+
import { EditorComponent } from "./pages/editor/editor.component";
88
import { ArticleComponent } from "./pages/article/article.component";
9+
import { ProfileComponent } from "./pages/profile/profile.component";
10+
import { FeedMenuEnum } from "./common/models/view/feed.view-model";
11+
import { ProfileRoutingData } from "./common/models/view/profile-routing-data.model";
912

1013
const routes: Routes = [
1114
{ path: '', component: HomeComponent },
@@ -18,11 +21,39 @@ const routes: Routes = [
1821
]
1922
},
2023
{ path: 'article/:slug', component: ArticleComponent },
24+
{
25+
path: 'profile/:username', children: [
26+
{
27+
path: '',
28+
component: ProfileComponent,
29+
data: { feedMenu: FeedMenuEnum.MINE } as ProfileRoutingData
30+
},
31+
{
32+
path: 'favorites',
33+
component: ProfileComponent,
34+
data: { feedMenu: FeedMenuEnum.FAVORITES } as ProfileRoutingData
35+
}
36+
]
37+
},
38+
{
39+
path: 'my-profile',
40+
children: [
41+
{
42+
path: '', component: ProfileComponent,
43+
data: { feedMenu: FeedMenuEnum.MINE } as ProfileRoutingData
44+
},
45+
{
46+
path: 'favorites', component: ProfileComponent,
47+
data: { feedMenu: FeedMenuEnum.FAVORITES } as ProfileRoutingData
48+
}
49+
],
50+
51+
},
2152
{ path: '**', redirectTo: ''}
2253
];
2354

2455
@NgModule({
25-
imports: [RouterModule.forRoot(routes, {useHash: true})],
56+
imports: [RouterModule.forRoot(routes, { useHash: true })],
2657
exports: [RouterModule]
2758
})
2859
export class AppRoutingModule { }

frontend/src/app/common/models/view/feed.view-model.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ export interface FeedMenu {
66

77
export enum FeedMenuEnum {
88
MINE = 'mine',
9-
GLOBAL = 'global'
9+
GLOBAL = 'global',
10+
FAVORITES = 'favorites'
1011
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { FeedMenuEnum } from "./feed.view-model";
2+
3+
export interface ProfileRoutingData {
4+
feedMenu: FeedMenuEnum;
5+
}

frontend/src/app/common/services/api/article.service.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import {Injectable} from '@angular/core';
2-
import {RequestHelperService} from "../utils/request-helper.service";
1+
import { Injectable } from '@angular/core';
2+
import { RequestHelperService } from "../utils/request-helper.service";
33
import {
44
ArticleResponse,
55
ArticlesResponse,
66
CreateArticlePayload,
7-
QueryArticlesParams, UpdateArticlePayload
7+
QueryArticlesParams,
8+
UpdateArticlePayload
89
} from "../../models/api/article.model";
9-
import {Observable} from "rxjs";
10-
import { QueryPaginationParams } from "../../models/api/query-pagination.model";
10+
import { Observable } from "rxjs";
1111

1212
@Injectable({
1313
providedIn: 'root'
@@ -38,7 +38,7 @@ export class ArticleService {
3838
return this._requestHelper.get('/articles', {params});
3939
}
4040

41-
public queryFeedArticles(params: QueryPaginationParams): Observable<ArticlesResponse> {
41+
public queryFeedArticles(params: QueryArticlesParams): Observable<ArticlesResponse> {
4242
return this._requestHelper.get('/articles/feed', {params});
4343
}
4444
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { TestBed } from '@angular/core/testing';
2+
3+
import { ProfileService } from './profile.service';
4+
import { RequestHelperService } from "../utils/request-helper.service";
5+
import { of } from "rxjs";
6+
7+
describe('ProfileService', () => {
8+
let service: ProfileService;
9+
10+
let spyRequestHelperService: Partial<jasmine.SpyObj<RequestHelperService>>;
11+
12+
beforeEach(() => {
13+
spyRequestHelperService = {
14+
get: jasmine.createSpy(),
15+
post: jasmine.createSpy(),
16+
delete: jasmine.createSpy()
17+
}
18+
spyRequestHelperService.get!.and.returnValue(of(null));
19+
spyRequestHelperService.post!.and.returnValue(of(null));
20+
spyRequestHelperService.delete!.and.returnValue(of(null));
21+
})
22+
23+
beforeEach(() => {
24+
TestBed.configureTestingModule({
25+
providers: [
26+
ProfileService,
27+
{ provide: RequestHelperService, useValue: spyRequestHelperService }
28+
]
29+
});
30+
service = TestBed.inject(ProfileService);
31+
});
32+
33+
it('should be created', () => {
34+
expect(service).toBeTruthy();
35+
});
36+
37+
it('should call getProfile', () => {
38+
const expectedUsername = 'username';
39+
service.getProfile(expectedUsername);
40+
expect(spyRequestHelperService.get).toHaveBeenCalledWith(`/profiles/${ expectedUsername }`);
41+
});
42+
43+
it('should call followUser', () => {
44+
const expectedUsername = 'username';
45+
service.followUser(expectedUsername);
46+
expect(spyRequestHelperService.post).toHaveBeenCalledWith(`/profiles/${ expectedUsername }/follow`, null);
47+
});
48+
49+
it('should call unfollowUser', () => {
50+
const expectedUsername = 'username';
51+
service.unfollowUser(expectedUsername);
52+
expect(spyRequestHelperService.delete).toHaveBeenCalledWith(`/profiles/${ expectedUsername }/follow`);
53+
});
54+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Injectable } from '@angular/core';
2+
import { RequestHelperService } from "../utils/request-helper.service";
3+
import { Observable } from "rxjs";
4+
import { ProfileResponse } from "../../models/api/profile.model";
5+
6+
@Injectable({
7+
providedIn: 'root'
8+
})
9+
export class ProfileService {
10+
11+
constructor(
12+
private readonly _requestHelper: RequestHelperService
13+
) { }
14+
15+
public getProfile(username: string): Observable<ProfileResponse> {
16+
return this._requestHelper.get(`/profiles/${username}`);
17+
}
18+
19+
public followUser(username: string): Observable<ProfileResponse> {
20+
return this._requestHelper.post(`/profiles/${username}/follow`, null);
21+
}
22+
23+
public unfollowUser(username: string): Observable<ProfileResponse> {
24+
return this._requestHelper.delete(`/profiles/${username}/follow`);
25+
}
26+
}

frontend/src/app/common/services/utils/request-helper.service.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,12 @@ export class RequestHelperService {
1414
private readonly _authService: AuthenticationService
1515
) { }
1616

17-
public get(url: string, params?: any): Observable<any> {
17+
public get(url: string, queryOptions: any = {}): Observable<any> {
1818
const options = {
19-
headers: this._constructRequestHeaders(),
20-
params
19+
...queryOptions,
20+
headers: this._constructRequestHeaders()
2121
}
2222

23-
if (!params) delete options.params;
24-
2523
return this._httpClient.get(this._decorateUrl(url), options);
2624
}
2725

frontend/src/app/pages/home/feed/feed.component.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
22
import { FeedMenuEnum } from "../../../common/models/view/feed.view-model";
33
import { ArticleService } from "../../../common/services/api/article.service";
4-
import { Article, ArticlesResponse } from "../../../common/models/api/article.model";
4+
import { Article, ArticlesResponse, QueryArticlesParams } from "../../../common/models/api/article.model";
55
import { Observable } from "rxjs";
6-
import { DEFAULT_PROFILE_IMAGE, QUERY_PAGE_SIZE } from "../../../common/constants/default.constant";
6+
import { QUERY_PAGE_SIZE } from "../../../common/constants/default.constant";
77

88
@Component({
99
selector: 'app-feed',
1010
templateUrl: './feed.component.html',
1111
styleUrl: './feed.component.scss'
1212
})
1313
export class FeedComponent implements OnChanges {
14-
public readonly DEFAULT_PROFILE_IMAGE = DEFAULT_PROFILE_IMAGE;
1514
@Input() feedMenuId?: FeedMenuEnum;
15+
@Input() queryParams?: Partial<QueryArticlesParams> = {};
1616

1717
public articles: Article[] = [];
1818
public activePageIndex = 0;
@@ -27,10 +27,13 @@ export class FeedComponent implements OnChanges {
2727
if (changes['feedMenuId']?.currentValue) {
2828
this._queryFeed();
2929
}
30+
31+
if (changes['queryParams']?.currentValue) {
32+
this._queryFeed();
33+
}
3034
}
3135

32-
private _queryFeed(pageIndex = 0): void {
33-
console.log(pageIndex)
36+
private _queryFeed(): void {
3437
this._constructQueryRequest().subscribe((response:ArticlesResponse) => {
3538
this.articles = response.articles;
3639
this.totalPages = Math.ceil(response.articlesCount / QUERY_PAGE_SIZE);
@@ -39,6 +42,7 @@ export class FeedComponent implements OnChanges {
3942

4043
private _constructQueryRequest(): Observable<ArticlesResponse> {
4144
const queryParams = {
45+
...this.queryParams,
4246
limit: QUERY_PAGE_SIZE,
4347
offset: this.activePageIndex * QUERY_PAGE_SIZE
4448
};
@@ -56,6 +60,6 @@ export class FeedComponent implements OnChanges {
5660
}
5761

5862
this.activePageIndex = pageIndex;
59-
this._queryFeed(this.activePageIndex);
63+
this._queryFeed();
6064
}
6165
}

frontend/src/app/pages/pages.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { EditorComponent } from './editor/editor.component';
1111
import { FeedComponent } from './home/feed/feed.component';
1212
import { ArticleComponent } from './article/article.component';
1313
import { ArticleMetaComponent } from './article/article-meta/article-meta.component';
14+
import { ProfileComponent } from './profile/profile.component';
1415

1516

1617

@@ -23,7 +24,8 @@ import { ArticleMetaComponent } from './article/article-meta/article-meta.compon
2324
EditorComponent,
2425
FeedComponent,
2526
ArticleComponent,
26-
ArticleMetaComponent
27+
ArticleMetaComponent,
28+
ProfileComponent
2729
],
2830
imports: [
2931
CommonModule,
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<div class="profile-page">
2+
<ng-container *ngIf="!isLoading">
3+
<div class="user-info">
4+
<div class="container">
5+
<ng-container *ngIf="profile; else notFound">
6+
<div class="row">
7+
<div class="col-xs-12 col-md-10 offset-md-1">
8+
<img [src]="profile.image || DEFAULT_PROFILE_IMAGE" class="user-img"/>
9+
<h4>{{ profile.username }}</h4>
10+
<div>
11+
{{ profile.bio }}
12+
</div>
13+
14+
<ng-container *ngIf="!isMyProfile">
15+
<button *ngIf="!profile.following" class="btn btn-sm btn-outline-secondary action-btn"
16+
(click)="follow()">
17+
<i class="ion-plus-round"></i>
18+
&nbsp; Follow {{ profile.username }}
19+
</button>
20+
21+
<button *ngIf="profile.following" class="btn btn-sm btn-secondary action-btn" (click)="unfollow()">
22+
<i class="ion-plus-round"></i>
23+
&nbsp; Unfollow {{ profile.username }}
24+
</button>
25+
</ng-container>
26+
27+
28+
<button *ngIf="isMyProfile" class="btn btn-sm btn-outline-secondary action-btn" routerLink="/settings">
29+
<i class="ion-gear-a"></i>
30+
&nbsp; Edit Profile Settings
31+
</button>
32+
</div>
33+
</div>
34+
</ng-container>
35+
36+
</div>
37+
</div>
38+
39+
<div class="container" *ngIf="profile">
40+
<div class="row">
41+
<div class="col-xs-12 col-md-10 offset-md-1">
42+
<div class="articles-toggle">
43+
<ul class="nav nav-pills outline-active">
44+
<li class="nav-item">
45+
<a class="nav-link" [ngClass]="{'active': feedQueryParams?.author}"
46+
routerLink="/profile/{{profile.username}}">My Articles</a>
47+
</li>
48+
<li class="nav-item">
49+
<a class="nav-link" [ngClass]="{'active': feedQueryParams?.favorited}"
50+
routerLink="/profile/{{profile.username}}/favorites">Favorited Articles</a>
51+
</li>
52+
</ul>
53+
</div>
54+
55+
<app-feed [queryParams]="feedQueryParams"></app-feed>
56+
</div>
57+
</div>
58+
</div>
59+
</ng-container>
60+
</div>
61+
62+
<ng-template #notFound>
63+
<div class="row">
64+
<div class="col-xs-12 col-md-10 offset-md-1">
65+
<h4>The profile you are looking for does not exist.</h4>
66+
</div>
67+
</div>
68+
</ng-template>

0 commit comments

Comments
 (0)