Skip to content

Commit

Permalink
Task 2
Browse files Browse the repository at this point in the history
  • Loading branch information
michaeldever committed Sep 19, 2023
1 parent ff2cbe9 commit f8d58a6
Show file tree
Hide file tree
Showing 15 changed files with 238 additions and 27 deletions.
3 changes: 2 additions & 1 deletion apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import { ProfileModule } from './profile/profile.module';
import { TagModule } from './tag/tag.module';
import { UserModule } from './user/user.module';
import ormConfig from '../mikro-orm.config';
import { RosterModule } from './roster/roster.module';
@Module({
controllers: [AppController],
imports: [MikroOrmModule.forRoot(ormConfig), ArticleModule, UserModule, ProfileModule, TagModule],
imports: [MikroOrmModule.forRoot(ormConfig), ArticleModule, UserModule, ProfileModule, TagModule, RosterModule],
providers: [],
})
export class AppModule implements NestModule, OnModuleInit {
Expand Down
9 changes: 6 additions & 3 deletions apps/backend/src/article/article.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@ export class Article {
@Property()
body = '';

@Property({ type: 'datetime', onUpdate(entity: Article) {
return entity.createdAt.toString().replace('T', ' ').replace('Z','')
}, })
@Property({
type: 'datetime',
onUpdate(entity: Article) {
return new Date(entity.createdAt).toISOString().replace('T', ' ').replace('Z', '');
},
})
createdAt = new Date();

@Property({ type: 'date', onUpdate: () => new Date() })
Expand Down
39 changes: 19 additions & 20 deletions apps/backend/src/article/article.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,40 +156,39 @@ export class ArticleService {
const article = new Article(user!, dto.title, dto.description, dto.body);

// Parse tags
const tagNames = dto.tagList.split(',');
const tagNames = dto.tagList.split(',');
article.tagList.push(...tagNames.map((name: string) => name.trim()));

// Remove duplicates
article.tagList = [...new Set(article.tagList)];
article.tagList = [...new Set(article.tagList)];

user?.articles.add(article);
await this.em.flush();

user?.articles.add(article);
await this.em.flush();
user?.articles.add(article);
await this.em.flush();

return { article: article.toJSON(user!) };
}

async update(userId: number, slug: string, articleData: CreateArticleDto): Promise<IArticleRO> {
const user = await this.userRepository.findOne(
{ id: userId },
{ populate: ['followers', 'favorites', 'articles'] },
);

const user = await this.userRepository.findOne(
{ id: userId },
{ populate: ['followers', 'favorites', 'articles'] }
);

const article = await this.articleRepository.findOne({ slug }, { populate: ['author'] });
const article = await this.articleRepository.findOne({ slug }, { populate: ['author'] });

const tagList = articleData.tagList.split(',');
wrap(article).assign({
...articleData,
tagList
});
const tagList = articleData.tagList.split(',');
wrap(article).assign({
...articleData,
tagList,
});

await this.em.flush();
await this.em.flush();

return { article: article!.toJSON(user!) };
}
return { article: article!.toJSON(user!) };
}

async delete(slug: string) {
return this.articleRepository.nativeDelete({ slug });
Expand Down
7 changes: 7 additions & 0 deletions apps/backend/src/roster/roster-user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface RosterUser {
username: string;
url: string;
articlesCount: number;
totalFavorites: number;
firstArticleDate?: Date;
}
12 changes: 12 additions & 0 deletions apps/backend/src/roster/roster.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { RosterService } from './roster.service';

@Controller('roster')
export class RosterController {
constructor(private readonly rosterService: RosterService) {}

@Get()
async getRoster() {
return this.rosterService.getRoster();
}
}
12 changes: 12 additions & 0 deletions apps/backend/src/roster/roster.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { RosterController } from './roster.controller';
import { RosterService } from './roster.service';
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { User } from '../user/user.entity';

@Module({
controllers: [RosterController],
imports: [MikroOrmModule.forFeature({ entities: [User] })],
providers: [RosterService],
})
export class RosterModule {}
46 changes: 46 additions & 0 deletions apps/backend/src/roster/roster.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Injectable } from '@nestjs/common';
import { User } from '../user/user.entity';
import { UserRepository } from '../user/user.repository';
import { Article } from '../article/article.entity';

export interface RosterUser {
username: string;
url: string;
articlesCount: number;
totalFavorites: number;
firstArticleDate?: Date;
}

@Injectable()
export class RosterService {
constructor(private userRepository: UserRepository) {}

async getRoster(): Promise<RosterUser[]> {
const users = await this.userRepository.findAll({
populate: ['articles'],
});

users.sort((a, b) => {
return (
b.articles.getItems().reduce((sum: number, article: Article) => {
return sum + article.favoritesCount;
}, 0) -
a.articles.getItems().reduce((sum: number, article: Article) => {
return sum + article.favoritesCount;
}, 0)
);
});

return users.map((user) => {
return {
username: user.username,
url: `/profile/${user.username}`,
articlesCount: user.articles.length,
totalFavorites: user.articles.getItems().reduce((sum, article) => {
return sum + article.favoritesCount;
}, 0),
firstArticleDate: user.articles[0]?.createdAt,
};
});
}
}
5 changes: 2 additions & 3 deletions apps/backend/src/tag/tag.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,14 @@ export class TagService {
constructor(@InjectRepository(Article) private articleRepository: EntityRepository<Article>) {}

async findAll() {

const articles = await this.articleRepository.findAll();

const tags = articles.reduce((acc: string[], article: Article) => {
return [...acc, ...article.tagList];
}, [] as string[]);

return {
tags: [...new Set(tags)]
}
tags: [...new Set(tags)],
};
}
}
5 changes: 5 additions & 0 deletions apps/frontend/src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ export const appConfig: ApplicationConfig = {
path: 'profile',
loadChildren: () => import('@realworld/profile/feature-profile').then((profile) => profile.PROFILE_ROUTES),
},
{
path: 'roster',
loadComponent: () =>
import('@realworld/home/src/lib/roster/roster.component').then((roster) => roster.RosterListComponent),
},
]),
provideStore({
auth: authFeature.reducer,
Expand Down
6 changes: 6 additions & 0 deletions apps/frontend/src/app/layout/navbar/navbar.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
<li class="nav-item">
<a class="nav-link" routerLink="/register" routerLinkActive="active">Sign up</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="/roster" routerLinkActive="active"> Roster </a>
</li>
</ul>

<!-- Logged in user -->
Expand All @@ -37,6 +40,9 @@
<i class="ion-gear-a"></i>&nbsp;Settings
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="/roster" routerLinkActive="active"> Roster </a>
</li>
<li class="nav-item">
<a
data-e2e-id="logedin-user"
Expand Down
34 changes: 34 additions & 0 deletions libs/home/src/lib/roster/roster.component.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
.roster-page {
padding: 20px;
}

.roster-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
}

.roster-table {
width: 100%;
border-collapse: collapse;
}

.roster-table th,
.roster-table td {
padding: 10px;
border: 1px solid #ddd;
text-align: left;
}

.roster-table th {
background-color: #f2f2f2;
font-weight: bold;
}

.roster-table tbody tr:nth-child(even) {
background-color: #f5f5f5;
}

.roster-table a {
color: #0077cc;
}
28 changes: 28 additions & 0 deletions libs/home/src/lib/roster/roster.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<div class="roster-page">
<h1 class="roster-title">Conduit Roster</h1>

<table class="roster-table">
<thead>
<tr>
<th>Name</th>
<th>Total Articles</th>
<th>Total Favourites</th>
<th>First Article Date</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of users | async">
<td>
<a [routerLink]="['/profile', user.username]">
{{ user.username }}
</a>
</td>
<td>{{ user.articlesCount }}</td>
<td>{{ user.totalFavorites }}</td>
<td>
{{ user.firstArticleDate | date }}
</td>
</tr>
</tbody>
</table>
</div>
36 changes: 36 additions & 0 deletions libs/home/src/lib/roster/roster.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { CommonModule } from '@angular/common';
import {
Component,
Input,
Output,
EventEmitter,
ChangeDetectionStrategy,
OnInit,
OnChanges,
SimpleChanges,
} from '@angular/core';
import { RosterService } from './roster.service';
import { RosterUser } from './roster.type';
import { Observable, Subject, Subscription } from 'rxjs';
import { RouterModule } from '@angular/router';

@Component({
selector: 'roster',
standalone: true,
templateUrl: './roster.component.html',
styleUrls: ['./roster.component.css'],
imports: [CommonModule, RouterModule],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RosterListComponent implements OnInit {
users = new Subject<RosterUser[]>();

constructor(private rosterService: RosterService) {}

ngOnInit() {
this.rosterService.getRoster().subscribe((roster) => {
console.log(roster);
this.users.next(roster);
});
}
}
16 changes: 16 additions & 0 deletions libs/home/src/lib/roster/roster.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// roster.service.ts

import { Injectable } from '@angular/core';
import { ApiService } from '@realworld/core/http-client';
import { Observable } from 'rxjs';

@Injectable({
providedIn: 'root',
})
export class RosterService {
constructor(private apiService: ApiService) {}

getRoster(): Observable<any> {
return this.apiService.get('/roster');
}
}
7 changes: 7 additions & 0 deletions libs/home/src/lib/roster/roster.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface RosterUser {
username: string;
url: string;
articlesCount: number;
totalFavorites: number;
firstArticleDate?: Date;
}

0 comments on commit f8d58a6

Please sign in to comment.