@@ -1,7 +1,4 @@ | |||
<div class="site"> | |||
<header> | |||
<app-header></app-header> | |||
</header> | |||
<main> | |||
<app-news-dashboard></app-news-dashboard> | |||
</main> |
@@ -1,14 +1,13 @@ | |||
@import "../variables.scss"; | |||
main { | |||
padding-left: $footer-height; | |||
padding-top: $footer-height; | |||
padding: 0px; | |||
margin: 0px; | |||
padding-bottom: $footer-height; | |||
} | |||
.site { | |||
height: auto; | |||
min-height: 100%; | |||
} | |||
footer { |
@@ -1,5 +1,6 @@ | |||
import { BrowserModule } from '@angular/platform-browser'; | |||
import { NgModule } from '@angular/core'; | |||
import { FormsModule } from '@angular/forms'; | |||
import { AppRoutingModule } from './app-routing.module'; | |||
import { AppComponent } from './app.component'; | |||
@@ -12,6 +13,7 @@ import { AllNewsDashboardComponent } from './news/all-news-dashboard/all-news-da | |||
import { TabsComponent } from './misc/tab/tabs/tabs.component'; | |||
import { TabComponent } from './misc/tab/tab/tab.component'; | |||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; | |||
import { NewsTeaserComponent } from './news/news-teaser/news-teaser.component'; | |||
@NgModule({ | |||
@@ -24,8 +26,10 @@ import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; | |||
AllNewsDashboardComponent, | |||
TabsComponent, | |||
TabComponent, | |||
NewsTeaserComponent, | |||
], | |||
imports: [ | |||
FormsModule, | |||
BrowserModule.withServerTransition({ appId: 'serverApp' }), | |||
AppRoutingModule, | |||
HttpClientModule, |
@@ -4,7 +4,7 @@ | |||
padding-left: 0.5%; | |||
padding-top: 0.5%; | |||
padding-bottom: 0.5%; | |||
background-color: $primary-color; | |||
background: transparent; | |||
margin: 0px; | |||
font-size: 20px; | |||
text-align: left; |
@@ -1,4 +0,0 @@ | |||
<div class="header"> | |||
<fa-icon [icon]="faNewspaper"></fa-icon> | |||
News Page | |||
</div> |
@@ -2,7 +2,7 @@ | |||
.header { | |||
color: white; | |||
background: linear-gradient(48deg, $detail-color, $secondary-color, $primary-color); | |||
background: transparent; | |||
margin: 0px; | |||
font-size: 30px; | |||
padding: 1em 1em; |
@@ -138,3 +138,8 @@ | |||
.inactive { | |||
display: none; | |||
} | |||
.pane { | |||
width: 100%; | |||
} |
@@ -1,5 +1,6 @@ | |||
import { Component, OnInit, ContentChildren, Input } from '@angular/core'; | |||
import { Component, OnInit, ContentChildren, Input, ViewChildren, TemplateRef } from '@angular/core'; | |||
import { TabsComponent } from '../tabs/tabs.component'; | |||
import { Template } from '@angular/compiler/src/render3/r3_ast'; | |||
@Component({ | |||
templateUrl: 'tab.component.html', |
@@ -1,15 +1,21 @@ | |||
<div class="tab-container"> | |||
<div class="flex row"> | |||
<div | |||
<div class="flex-container header"> | |||
<p class="header-icon"> | |||
<fa-icon [icon]="faNewspaper"></fa-icon> | |||
</p> | |||
<p class="title">News Page</p> | |||
<p class="spacer"></p> | |||
<p class="spacer"></p> | |||
<p | |||
*ngFor="let tab of tabs" | |||
(click)="selectTab(tab)" | |||
[class.active]="tab?.active" | |||
class="flex col tab-title" | |||
class="tab-title" | |||
> | |||
<p>{{ tab?.title }}</p> | |||
</div> | |||
{{ tab?.title }} | |||
</p> | |||
</div> | |||
<div class="tab-content flex row" > | |||
<ng-content></ng-content> | |||
<div class="tab-content flex flex-row"> | |||
<ng-content></ng-content> | |||
</div> | |||
</div> |
@@ -1,35 +1,51 @@ | |||
$border-radius: 12.5px; | |||
@import "../../../../variables.scss"; | |||
.tab-title { | |||
min-width: 6em; | |||
cursor: pointer; | |||
padding-left: 5px; | |||
padding-right: 5px; | |||
border-top: solid $border-color 1px; | |||
margin-bottom: 0px; | |||
border-top-left-radius: 12.5px; | |||
border-top-right-radius: 12.5px; | |||
background-color: $primary-color; | |||
opacity: 0.4; | |||
transition: 0.3s; | |||
padding: 1em; | |||
} | |||
.title { | |||
justify-self: flex-start; | |||
align-items: self-start; | |||
} | |||
.tab-title:active, .active { | |||
.tab-title:active, | |||
.active { | |||
opacity: 1; | |||
} | |||
.tab-title:hover { | |||
opacity: 0.8 | |||
opacity: 0.8; | |||
} | |||
.tab-content { | |||
border: 5px solid $primary-color; | |||
border-top-right-radius: 12.5px; | |||
border-top-right-radius: 12.5px; | |||
border-bottom-left-radius: 12.5px; | |||
border-bottom-right-radius: 12.5px; | |||
padding-left: 1em; | |||
height: 100%; | |||
} | |||
.tab-container { | |||
border-radius: $border-radius; | |||
background: white; | |||
} | |||
.header-icon { | |||
padding: 1em; | |||
} | |||
.header { | |||
justify-content: space-between; | |||
align-items: spac; | |||
height: 3em; | |||
flex-direction: row nowrap; | |||
font-size: $header-font-size; | |||
color: white; | |||
background: linear-gradient(48deg, $detail-color, $secondary-color, $primary-color); | |||
} |
@@ -1,5 +1,7 @@ | |||
import { Component, OnInit, ContentChildren, QueryList, AfterContentInit } from '@angular/core'; | |||
import { TabComponent } from '../tab/tab.component'; | |||
import { faNewspaper, faFilter } from '@fortawesome/free-solid-svg-icons'; | |||
import { from } from 'rxjs'; | |||
@Component({ | |||
selector: 'app-tabs', | |||
@@ -7,7 +9,7 @@ import { TabComponent } from '../tab/tab.component'; | |||
styleUrls: ['./tabs.component.scss'] | |||
}) | |||
export class TabsComponent implements OnInit, AfterContentInit { | |||
faNewspaper = faNewspaper; | |||
@ContentChildren(TabComponent) tabs: QueryList<TabComponent>; | |||
@ContentChildren(TabComponent) activeTab: TabComponent; | |||
public selectTab(tab: TabComponent): void { | |||
@@ -22,7 +24,6 @@ export class TabsComponent implements OnInit, AfterContentInit { | |||
} | |||
protected getTabAnimationDirection(tab: TabComponent): 'right' | 'left' | 'none' { | |||
debugger; | |||
const activeTab = this.tabs.toArray().filter(tab => tab.active)[0]; | |||
if (activeTab === undefined) { | |||
return 'none'; | |||
@@ -37,6 +38,10 @@ export class TabsComponent implements OnInit, AfterContentInit { | |||
return 'none'; | |||
} | |||
getActiveTab(): TabComponent { | |||
return this.tabs.toArray().filter(tab => tab.active)[0]; | |||
} | |||
ngAfterContentInit(): void { | |||
// get all active tabs | |||
this.tabs.toArray().forEach((tab: TabComponent, index) => tab.tabIndex = index); |
@@ -1,10 +0,0 @@ | |||
export class TopHeadlineSearchDto { | |||
sources: string; | |||
q: string; | |||
qInTitle: string; | |||
domains: string; | |||
excludeDomains: string; | |||
from: Date; | |||
to: Date; | |||
} |
@@ -0,0 +1,15 @@ | |||
import { SortBy } from '../enum/sort-by'; | |||
export class NewsSearchDto { | |||
sources?: string; | |||
q?: string; | |||
qInTitle?: string; | |||
domains?: string; | |||
excludeDomains?: string; | |||
from?: Date; | |||
to?: Date; | |||
sortBy?: SortBy; | |||
pageSize?: number; | |||
page?: number; | |||
apiKey: string; | |||
} |
@@ -0,0 +1,5 @@ | |||
export enum SortBy { | |||
'Relevancy' = 'relevancy', | |||
'Popularity' = 'popularity', | |||
'PublishedAt' = 'publishedAt', | |||
} |
@@ -1 +1,5 @@ | |||
<p>all-news-dashboard works!</p> | |||
<div class="flex-container wrap"> | |||
<div class="flex-item" *ngFor="let article of news"> | |||
<app-news-teaser [article]="article"></app-news-teaser> | |||
</div> | |||
</div> |
@@ -1,4 +1,7 @@ | |||
import { Component, OnInit } from '@angular/core'; | |||
import { NewsService } from '../../service/news.service'; | |||
import { Article } from '../../models/model/article'; | |||
import { apiKey } from '../../../environments/.api-key'; | |||
@Component({ | |||
selector: 'app-all-news-dashboard', | |||
@@ -7,9 +10,16 @@ import { Component, OnInit } from '@angular/core'; | |||
}) | |||
export class AllNewsDashboardComponent implements OnInit { | |||
constructor() { } | |||
news: Article[]; | |||
constructor(protected newsService: NewsService) { } | |||
ngOnInit(): void { | |||
// this.newsService.searchAllNews({ | |||
// apiKey | |||
// }).subscribe(response => { | |||
// this.news = response.articles; | |||
// }); | |||
} | |||
} |
@@ -1,8 +1,8 @@ | |||
<app-tabs> | |||
<app-tab title="Top News"> | |||
<app-tab style="width: auto" title="Top News"> | |||
<app-top-news-dashboard></app-top-news-dashboard> | |||
</app-tab> | |||
<app-tab title="All News"> | |||
<app-tab style="width: auto" title="All News"> | |||
<app-all-news-dashboard></app-all-news-dashboard> | |||
</app-tab> | |||
</app-tabs> |
@@ -0,0 +1,9 @@ | |||
@import "../../../variables.scss"; | |||
$border-radius: 12.5px; | |||
.dashboard { | |||
padding: 0px; | |||
background: white; | |||
} | |||
@@ -0,0 +1,19 @@ | |||
<div | |||
(click)="open()" | |||
class="teaser" | |||
*ngIf="article.urlToImage && article.content" | |||
> | |||
<div class="teaser-header"> | |||
<div class="teaser-title">{{ article.title }}</div> | |||
<div class="teaser-img"> | |||
<img | |||
[src]="article.urlToImage" | |||
*ngIf="article.urlToImage" | |||
alt="image of article" | |||
/> | |||
</div> | |||
</div> | |||
<div class="teaser-content" *ngIf="article.description"> | |||
{{ article.description }} ... <a [href]="article.url"> Read</a> | |||
</div> | |||
</div> |
@@ -0,0 +1,86 @@ | |||
@import "../../../variables.scss"; | |||
$teaser-border-radius: 12.5px; | |||
$teaser-background: linear-gradient(48deg, rgb(223, 217, 217), white); | |||
.teaser-header { | |||
min-height: 10em; | |||
flex-direction: column; | |||
> div.teaser-img { | |||
border-top-left-radius: $teaser-border-radius; | |||
border-top-right-radius: $teaser-border-radius; | |||
background: $teaser-background; | |||
display: flex; | |||
justify-content: center; | |||
align-items: center; | |||
> img { | |||
padding: 5px; | |||
max-width: 40em; | |||
max-height: 20em; | |||
} | |||
} | |||
> div.teaser-title { | |||
padding: 2em; | |||
background: transparent; | |||
max-width: 40em; | |||
text-align: center; | |||
font-size: $header-font-size; | |||
} | |||
> div.teaser-title::after { | |||
content: ""; | |||
background-color: $detail-color; | |||
display: block; | |||
margin: 2rem 0; | |||
height: 2px; | |||
width: 3.75rem; | |||
} | |||
} | |||
.teaser { | |||
cursor: pointer; | |||
border: 1px solid $border-color; | |||
width: 45em; | |||
min-height: 20em; | |||
border-radius: $teaser-border-radius; | |||
} | |||
@-webkit-keyframes hover { | |||
100% { | |||
border: 1px white; | |||
} | |||
} | |||
@keyframes hover { | |||
100% { | |||
border: 1px solid white; | |||
} | |||
} | |||
.teaser:hover { | |||
-webkit-animation: hover 0.3s cubic-bezier(0.39, 0.575, 0.565, 1) both; | |||
animation: hover 0.3s cubic-bezier(0.39, 0.575, 0.565, 1) both; | |||
} | |||
.teaser-content { | |||
background: $teaser-background; | |||
border-bottom-left-radius: $teaser-border-radius; | |||
border-bottom-right-radius: $teaser-border-radius; | |||
min-width: 10em; | |||
padding: 5px; | |||
} | |||
.teaser-footer { | |||
height: 2em; | |||
padding: 5px; | |||
} | |||
.btn { | |||
border-radius: 2.5px; | |||
background-color: $button-color; | |||
color: white; | |||
padding: 5px; | |||
} | |||
a { | |||
color: $detail-color; | |||
text-decoration: none; | |||
} |
@@ -0,0 +1,25 @@ | |||
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; | |||
import { NewsTeaserComponent } from './news-teaser.component'; | |||
describe('NewsTeaserComponent', () => { | |||
let component: NewsTeaserComponent; | |||
let fixture: ComponentFixture<NewsTeaserComponent>; | |||
beforeEach(async(() => { | |||
TestBed.configureTestingModule({ | |||
declarations: [ NewsTeaserComponent ] | |||
}) | |||
.compileComponents(); | |||
})); | |||
beforeEach(() => { | |||
fixture = TestBed.createComponent(NewsTeaserComponent); | |||
component = fixture.componentInstance; | |||
fixture.detectChanges(); | |||
}); | |||
it('should create', () => { | |||
expect(component).toBeTruthy(); | |||
}); | |||
}); |
@@ -0,0 +1,21 @@ | |||
import { Component, OnInit, Input } from '@angular/core'; | |||
import { Article } from '../../models/model/article'; | |||
@Component({ | |||
selector: 'app-news-teaser', | |||
templateUrl: './news-teaser.component.html', | |||
styleUrls: ['./news-teaser.component.scss'] | |||
}) | |||
export class NewsTeaserComponent implements OnInit { | |||
@Input() article: Article; | |||
constructor() { } | |||
ngOnInit(): void { | |||
} | |||
open(): void { | |||
window.open(this.article.url); | |||
} | |||
} |
@@ -1 +1,68 @@ | |||
<p>top-news-dashboard works!</p> | |||
<div class="title flex-container"> | |||
Top | |||
<ng-container *ngIf="selectedCategory"> | |||
{{ selectedCategory.key }} | |||
</ng-container> | |||
News | |||
<ng-container *ngIf="selectedCountry" | |||
>in {{ selectedCountry.key }} | |||
<img | |||
class="flag" | |||
src="https://www.countryflags.io/{{ | |||
selectedCountry.value | |||
}}/shiny/64.png" | |||
/></ng-container> | |||
</div> | |||
<div class="flex-container filter-bar" *ngIf="news"> | |||
<div class="filter"> | |||
<label for="search">Search: </label> | |||
<input | |||
(change)="upateDto()" | |||
name="search" | |||
id="search" | |||
[(ngModel)]="searchDto.q" | |||
/> | |||
</div> | |||
<div class="filter"> | |||
<label for="country">Country: </label> | |||
<select | |||
(change)="upateDto()" | |||
name="country" | |||
id="country" | |||
[(ngModel)]="selectedCountry" | |||
> | |||
<option | |||
*ngFor="let country of CountryCodes | keyvalue" | |||
[ngValue]="country" | |||
> | |||
{{ country.key }} | |||
</option> | |||
</select> | |||
</div> | |||
<div class="filter"> | |||
<label for="category">Category: </label> | |||
<select | |||
(change)="upateDto()" | |||
name="category" | |||
id="category" | |||
[(ngModel)]="selectedCategory" | |||
> | |||
<option *ngFor="let category of Category | keyvalue" [ngValue]="category"> | |||
{{ category.key }} | |||
</option> | |||
</select> | |||
</div> | |||
</div> | |||
<div class="flex-container wrap" *ngIf="news?.length; else notingFound"> | |||
<div class="flex-item" *ngFor="let article of news"> | |||
<app-news-teaser [article]="article"></app-news-teaser> | |||
</div> | |||
</div> | |||
<ng-template #notingFound> | |||
<div class="flex-container not-found"> | |||
noting found | |||
</div> | |||
</ng-template> |
@@ -0,0 +1,31 @@ | |||
@import "../../../variables.scss"; | |||
.filter-bar { | |||
height: 5em; | |||
margin: 2em; | |||
display: flex; | |||
flex-flow: row; | |||
justify-content: space-evenly; | |||
align-items: center; | |||
border: 1px solid $border-color; | |||
} | |||
.filter { | |||
height: 2em; | |||
} | |||
.title { | |||
background: transparent; | |||
font-size: $header-font-size; | |||
} | |||
.flag { | |||
height: 1em; | |||
padding-left: 0.5em; | |||
align-self: center; | |||
} | |||
.not-found { | |||
width: 160em; | |||
height: 10em; | |||
} |
@@ -1,8 +1,10 @@ | |||
import { Component, OnInit } from '@angular/core'; | |||
import { Component, OnInit, ContentChild } from '@angular/core'; | |||
import { Article } from '../../models/model/article'; | |||
import { NewsService } from '../../service/news.service'; | |||
import { apiKey } from '../../../environments/.api-key'; | |||
import { CountryCode } from '../../models/enum/country-codes'; | |||
import { TopHeadlineSearchDto } from '../../models/dto/top-headline-search.dto'; | |||
import { Category } from 'src/app/models/enum/category'; | |||
@Component({ | |||
selector: 'app-top-news-dashboard', | |||
@@ -10,16 +12,39 @@ import { CountryCode } from '../../models/enum/country-codes'; | |||
styleUrls: ['./top-news-dashboard.component.scss'] | |||
}) | |||
export class TopNewsDashboardComponent implements OnInit { | |||
news: Article[]; | |||
searchDto: TopHeadlineSearchDto = { apiKey }; | |||
selectedCountry: { key: string, value: CountryCode }; | |||
selectedCategory: { key: string, value: Category }; | |||
CountryCodes = CountryCode; | |||
Category = Category; | |||
constructor(protected newsService: NewsService) { } | |||
ngOnInit(): void { | |||
this.newsService.getTopNews({ | |||
apiKey, | |||
country: CountryCode.Germany | |||
}).subscribe(response => { | |||
upateDto(): void { | |||
if (this.selectedCountry?.value) { | |||
this.searchDto.country = this.selectedCountry.value; | |||
} else { | |||
delete this.searchDto.country; | |||
} | |||
if (this.selectedCategory?.value) { | |||
this.searchDto.category = this.selectedCategory.value; | |||
} else { | |||
delete this.searchDto.category; | |||
} | |||
this.search(); | |||
} | |||
search(): void { | |||
this.newsService.searchTopNews( | |||
this.searchDto | |||
).subscribe(response => { | |||
this.news = response.articles; | |||
}); | |||
} | |||
ngOnInit(): void { | |||
this.selectedCountry = { key: 'Germany', value: CountryCode.Germany }; | |||
this.upateDto(); | |||
} | |||
} |
@@ -1,10 +1,10 @@ | |||
import { HttpClient } from '@angular/common/http'; | |||
import { Injectable } from '@angular/core'; | |||
import { HttpClient, HttpParams, HttpResponse, HttpHeaders } from '@angular/common/http'; | |||
import { Observable } from 'rxjs'; | |||
import { environment } from '../../environments/environment'; | |||
import { TopHeadlineSearchDto } from '../models/dto/top-headline-search.dto'; | |||
import { NewsSearchDto } from '../models/dto/news-search.dto'; | |||
import { NewsResponse } from '../models/response/news-response'; | |||
import { environment } from '../../environments/environment'; | |||
import { Observable, Subject } from 'rxjs'; | |||
import { map, tap } from 'rxjs/operators'; | |||
@Injectable({ | |||
providedIn: 'root' | |||
@@ -13,9 +13,11 @@ export class NewsService { | |||
constructor(protected http: HttpClient) { } | |||
getTopNews(searchDto: TopHeadlineSearchDto): Observable<NewsResponse> | null { | |||
return new Subject<NewsResponse>(); | |||
const headers = new HttpHeaders(); | |||
return this.http.get<NewsResponse>(`${environment.apiUrl}/top-headlines`, { params: searchDto as any, headers }); | |||
searchTopNews(searchDto: TopHeadlineSearchDto): Observable<NewsResponse> | null { | |||
return this.http.get<NewsResponse>(`${environment.apiUrl}/top-headlines`, { params: searchDto as any }); | |||
} | |||
searchAllNews(searchDto: NewsSearchDto): Observable<NewsResponse> | null { | |||
return this.http.get<NewsResponse>(`${environment.apiUrl}/everything`, { params: searchDto as any }); | |||
} | |||
} |
@@ -1,20 +1,52 @@ | |||
@import "./variables.scss"; | |||
.flex { | |||
display: flex; | |||
.container { | |||
> .container { | |||
flex-direction: column; | |||
} | |||
.col { | |||
> .col { | |||
flex-direction: column; | |||
} | |||
.row { | |||
flex-direction: row; | |||
} | |||
.content { | |||
> .content { | |||
flex: 1; | |||
} | |||
} | |||
.flex-row { | |||
flex-direction: row; | |||
} | |||
.flex-container { | |||
padding: 0; | |||
margin: 0; | |||
list-style: none; | |||
display: -webkit-box; | |||
display: -moz-box; | |||
display: -ms-flexbox; | |||
display: -moz-flex; | |||
display: -webkit-flex; | |||
display: flex; | |||
justify-content: center; | |||
align-items: center; | |||
} | |||
.nowrap { | |||
-webkit-flex-wrap: nowrap; | |||
flex-wrap: nowrap; | |||
} | |||
.spacer { | |||
flex-grow: 1; | |||
} | |||
.wrap { | |||
-webkit-flex-wrap: wrap; | |||
flex-wrap: wrap; | |||
} | |||
.flex-item { | |||
padding: 5px; | |||
} | |||
.col > div { | |||
margin: 10px; | |||
padding: 20px; | |||
@@ -27,8 +59,8 @@ | |||
body, | |||
html { | |||
background-color: $base-color; | |||
font-size: $default-font-size; | |||
background: linear-gradient(48deg, $detail-color, $secondary-color, $primary-color); | |||
height: 100%; | |||
margin: 0px; | |||
} | |||
@@ -4,10 +4,17 @@ $primary-color: #2f85f6; | |||
$secondary-color: #8c3cee; | |||
$detail-color: #8c3cee; | |||
$button-color: #8a8095; | |||
$text-primary: white; | |||
$border-color: white; | |||
$border-color: #d8d8d8; | |||
$footer-height: 1.2em; | |||
$default-font-size: 15px; | |||
$header-font-size: 25px; |