{ 16 | const newParams = new HttpParams({ fromObject: params }).toString(); 17 | return this.http.get( 18 | `${environment.baseApi}/articles?${newParams}` 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /libs/global-services/src/lib/dateago/dateago.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { DateagoPipe } from './dateago.pipe'; 2 | 3 | describe('DateagoPipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new DateagoPipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /libs/global-services/src/lib/dateago/dateago.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'dateago', 5 | }) 6 | export class DateagoPipe implements PipeTransform { 7 | transform(value: Date, args?: unknown[]): string { 8 | if (value) { 9 | const seconds = Math.floor((Date.now() - +new Date(value)) / 1000); 10 | if (seconds < 59) 11 | // less than 59 seconds ago will show as 'Just now' 12 | return 'Just now'; 13 | const intervals = { 14 | day: 86400, 15 | hour: 3600, 16 | minute: 60, 17 | }; 18 | switch (true) { 19 | case seconds < intervals.minute: { 20 | return '(Just Now)'; 21 | } 22 | case seconds < intervals.hour: { 23 | return `(${Math.floor(seconds / intervals.minute)} minute's ago)`; 24 | } 25 | case seconds < intervals.day: { 26 | return `(${Math.floor(seconds / intervals.hour)} + hours's ago`; 27 | } 28 | } 29 | } 30 | return ''; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /libs/global-services/src/lib/user-articles/user-articles.store.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { UserArticlesStore } from './user-articles.store'; 4 | 5 | describe('UserArticlesService', () => { 6 | let service: UserArticlesStore; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(UserArticlesStore); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /libs/global-services/src/lib/user-articles/user-articles.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Article } from '@devto/core/models'; 3 | import { ComponentStore, tapResponse } from '@ngrx/component-store'; 4 | import { Observable } from 'rxjs'; 5 | import { switchMap } from 'rxjs/operators'; 6 | import { ArticleApiService } from '@devto/global-services'; 7 | 8 | interface ArticlesState { 9 | articles: Article[]; 10 | } 11 | @Injectable({ 12 | providedIn: 'root', 13 | }) 14 | export class UserArticlesStore extends ComponentStore { 15 | readonly articles$ = this.select((state) => state.articles); 16 | 17 | readonly setArticles = this.updater( 18 | (state: ArticlesState, articles: Article[]) => { 19 | return { 20 | ...state, 21 | articles, 22 | }; 23 | } 24 | ); 25 | 26 | readonly getArticles = this.effect( 27 | (params: Observable>) => 28 | params.pipe( 29 | switchMap((params) => 30 | this.articleApiS.getArticles({ username: params.username }).pipe( 31 | tapResponse( 32 | (articles) => this.setArticles(articles), 33 | (error) => this.logError(error) 34 | ) 35 | ) 36 | ) 37 | ) 38 | ); 39 | 40 | constructor(private articleApiS: ArticleApiService) { 41 | super({ articles: [] }); 42 | } 43 | 44 | logError(error: unknown) { 45 | console.error(error); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /libs/global-services/src/lib/user/user-api.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { UserApiService } from './user-api.service'; 4 | 5 | describe('UserApiService', () => { 6 | let service: UserApiService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(UserApiService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /libs/global-services/src/lib/user/user-api.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { UserDetails } from '@devto/core/models'; 5 | import { environment } from '@devto/environments'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class UserApiService { 11 | constructor(private http: HttpClient) {} 12 | 13 | getUser(username?: string): Observable { 14 | return this.http.get( 15 | `${environment.baseApi}/users/by_username?url=${username}` 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /libs/global-services/src/lib/user/user.store.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { UserStore } from './user.store'; 4 | 5 | describe('UserService', () => { 6 | let service: UserStore; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(UserStore); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /libs/global-services/src/lib/user/user.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ComponentStore, tapResponse } from '@ngrx/component-store'; 3 | import { Observable } from 'rxjs'; 4 | import { switchMap } from 'rxjs/operators'; 5 | import { UserDetails } from '@devto/core/models'; 6 | import { UserApiService } from './user-api.service'; 7 | 8 | interface UserState { 9 | user: UserDetails | null; 10 | } 11 | 12 | @Injectable() 13 | export class UserStore extends ComponentStore { 14 | readonly user$ = this.select((state) => state.user); 15 | readonly setUser = this.updater((state: UserState, user: UserDetails) => ({ 16 | ...state, 17 | user, 18 | })); 19 | readonly getUser = this.effect((username: Observable) => 20 | username.pipe( 21 | switchMap((username) => 22 | this.userApiS.getUser(username).pipe( 23 | tapResponse( 24 | (user) => this.setUser(user), 25 | (error) => this.logError(error) 26 | ) 27 | ) 28 | ) 29 | ) 30 | ); 31 | 32 | constructor(private userApiS: UserApiService) { 33 | super({ user: null }); 34 | } 35 | 36 | private logError(error: unknown) { 37 | console.error(error); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /libs/global-services/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /libs/global-services/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true 18 | }, 19 | "angularCompilerOptions": { 20 | "strictInjectionParameters": true, 21 | "strictInputAccessModifiers": true, 22 | "strictTemplates": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /libs/global-services/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"], 13 | "include": ["**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /libs/global-services/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/global-services/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../dist/out-tsc", 6 | "module": "commonjs", 7 | "types": [ 8 | "jest", 9 | "node" 10 | ] 11 | }, 12 | "files": [ 13 | "src/test-setup.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /libs/home/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": ["devto","app"], 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": ["devto","app"], 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nrwl/nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /libs/home/README.md: -------------------------------------------------------------------------------- 1 | # home 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test home` to execute the unit tests. 8 | -------------------------------------------------------------------------------- /libs/home/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'home', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | stringifyContentPathRegex: '\\.(html|svg)$', 9 | }, 10 | }, 11 | coverageDirectory: '../../coverage/libs/home', 12 | transform: { 13 | '^.+\\.(ts|js|html)$': 'jest-preset-angular', 14 | }, 15 | snapshotSerializers: [ 16 | 'jest-preset-angular/build/serializers/no-ng-attributes', 17 | 'jest-preset-angular/build/serializers/ng-snapshot', 18 | 'jest-preset-angular/build/serializers/html-comment', 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /libs/home/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/libs/home", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/home/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@devto/home", 3 | "version": "0.0.1", 4 | "peerDependencies": { 5 | "@angular/common": "^12.1.1", 6 | "@angular/core": "^12.1.1" 7 | }, 8 | "dependencies": { 9 | "tslib": "^2.2.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /libs/home/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/home.module'; 2 | -------------------------------------------------------------------------------- /libs/home/src/lib/articles/article-container/article-container.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ArticleContainerComponent } from './article-container.component'; 4 | 5 | describe('ArticleContainerComponent', () => { 6 | let component: ArticleContainerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ArticleContainerComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ArticleContainerComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/home/src/lib/articles/article-container/article-container.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ArticleStore } from '../services/article.store'; 3 | 4 | @Component({ 5 | selector: 'app-article-container', 6 | template: ` 7 | 10 | 11 | 12 | `, 13 | }) 14 | export class ArticleContainerComponent { 15 | articles$ = this.articleStore.articles$; 16 | featuredArticle$ = this.articleStore.featuredArticle$; 17 | 18 | constructor(private articleStore: ArticleStore) {} 19 | } 20 | -------------------------------------------------------------------------------- /libs/home/src/lib/articles/article-header/article-header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ArticleHeaderComponent } from './article-header.component'; 4 | 5 | describe('ArticleHeaderComponent', () => { 6 | let component: ArticleHeaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ArticleHeaderComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ArticleHeaderComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/home/src/lib/articles/article-header/article-header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ARTICLE_HEADER_TABS } from '@devto/global-constants'; 3 | 4 | @Component({ 5 | selector: 'app-article-header', 6 | template: ` 7 | Posts 8 | 9 | 10 | 11 | 12 | {{ 13 | tab | titlecase 14 | }} 15 | 16 | 17 | 18 | `, 19 | styles: [ 20 | ` 21 | .tabs-list { 22 | margin: 0; 23 | padding: 0; 24 | } 25 | 26 | .tab-item { 27 | padding: 0.5rem; 28 | margin: 0 0.25rem; 29 | flex-direction: column; 30 | font-size: 1rem; 31 | transition: all cubic-bezier(0.17, 0.67, 0.5, 0.71) 100ms; 32 | border-radius: 5px; 33 | &.active { 34 | font-weight: 500; 35 | color: #08090a; 36 | } 37 | &:before { 38 | content: attr(data-text); 39 | height: 0; 40 | visibility: hidden; 41 | overflow: hidden; 42 | user-select: none; 43 | pointer-events: none; 44 | } 45 | &:after { 46 | position: absolute; 47 | content: ''; 48 | } 49 | } 50 | `, 51 | ], 52 | }) 53 | export class ArticleHeaderComponent { 54 | selectedTab = 'feed'; 55 | tabs = ARTICLE_HEADER_TABS; 56 | } 57 | -------------------------------------------------------------------------------- /libs/home/src/lib/articles/featured-article/featured-article.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FeaturedArticleComponent } from './featured-article.component'; 4 | 5 | describe('FeaturedArticleComponent', () => { 6 | let component: FeaturedArticleComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [FeaturedArticleComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(FeaturedArticleComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/home/src/lib/articles/featured-article/featured-article.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { Article } from '@devto/core/models'; 3 | 4 | @Component({ 5 | selector: 'app-featured-article', 6 | template: ` 13 | 14 | 15 | 21 | {{ featured?.title }} 22 | 23 | `, 24 | styles: [ 25 | ` 26 | :host { 27 | display: block; 28 | background-color: #fff; 29 | box-shadow: 0 0 0 1px rgba(8, 9, 10, 0.1); 30 | border-radius: 5px; 31 | cursor: pointer; 32 | margin: 0 0 0.75rem; 33 | overflow: hidden; 34 | &:focus-within { 35 | outline: none; 36 | box-shadow: 0 0 0 2px #3b49df; 37 | } 38 | } 39 | 40 | app-article-card { 41 | box-shadow: none; 42 | margin: 0 0 0; 43 | &:focus-within { 44 | outline: none; 45 | box-shadow: none; 46 | } 47 | } 48 | 49 | .featured-img { 50 | display: block; 51 | width: 100%; 52 | height: auto; 53 | padding-bottom: 42%; 54 | background-size: cover; 55 | background-position: center center; 56 | } 57 | 58 | .featured-title { 59 | font-size: 1.875rem; 60 | } 61 | `, 62 | ], 63 | }) 64 | export class FeaturedArticleComponent { 65 | @Input() featured!: Article; 66 | } 67 | -------------------------------------------------------------------------------- /libs/home/src/lib/articles/services/article.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ArticleStore } from './article.store'; 4 | 5 | describe('ArticleStore', () => { 6 | let service: ArticleStore; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ArticleStore); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /libs/home/src/lib/articles/services/article.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ComponentStore, tapResponse } from '@ngrx/component-store'; 3 | import { ArticleApiService } from '@devto/global-services'; 4 | import { Article } from '@devto/core/models'; 5 | 6 | interface ArticlesState { 7 | articles: Article[]; 8 | featured?: Article; 9 | } 10 | 11 | @Injectable({ 12 | providedIn: 'root', 13 | }) 14 | export class ArticleStore extends ComponentStore { 15 | readonly articles$ = this.select((state) => state.articles); 16 | readonly featuredArticle$ = this.select((state) => state.featured); 17 | readonly setArticles = this.updater( 18 | (state: ArticlesState, articles: Article[]) => { 19 | let index = 0; 20 | articles.find((article, idx) => { 21 | index = idx; 22 | return article.cover_image; 23 | }); 24 | let featured = articles.splice(index, 1)[0]; 25 | return { 26 | ...state, 27 | featured: featured, 28 | articles: articles, 29 | }; 30 | } 31 | ); 32 | readonly getArticles = this.effect(() => 33 | this.articleApiS.getArticles({ state: 'rising' }).pipe( 34 | tapResponse( 35 | (articles) => this.setArticles(articles), 36 | (error) => this.logError(error) 37 | ) 38 | ) 39 | ); 40 | constructor(private articleApiS: ArticleApiService) { 41 | super({ articles: [] }); 42 | } 43 | 44 | logError(error: unknown) { 45 | console.error(error); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /libs/home/src/lib/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { RouterTestingModule } from "@angular/router/testing"; 4 | 5 | import { ArticleContainerComponent } from './articles/article-container/article-container.component'; 6 | import { HomeComponent } from './home.component'; 7 | import { RightbarContainerComponent } from './rightbar/rightbar-container/rightbar-container.component'; 8 | import { SidebarComponent } from './sidebar/sidebar/sidebar.component'; 9 | import { HomeModule } from './home.module'; 10 | 11 | describe('HomeComponent', () => { 12 | let component: HomeComponent; 13 | let fixture: ComponentFixture; 14 | 15 | beforeEach(async () => { 16 | await TestBed.configureTestingModule({ 17 | declarations: [ 18 | HomeComponent, 19 | SidebarComponent, 20 | ArticleContainerComponent, 21 | RightbarContainerComponent, 22 | ], 23 | imports: [HomeModule, HttpClientTestingModule, RouterTestingModule], 24 | }).compileComponents(); 25 | }); 26 | 27 | beforeEach(() => { 28 | fixture = TestBed.createComponent(HomeComponent); 29 | component = fixture.componentInstance; 30 | fixture.detectChanges(); 31 | }); 32 | 33 | it('should create', () => { 34 | expect(component).toBeTruthy(); 35 | }); 36 | 37 | it('should render', () => { 38 | expect(fixture.nativeElement).toMatchSnapshot(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /libs/home/src/lib/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-home', 5 | template: ` 6 | 7 | `, 8 | styles: [ 9 | ` 10 | :host { 11 | display: grid; 12 | grid-gap: 1rem; 13 | grid-template-columns: 240px 2fr 1fr; 14 | } 15 | `, 16 | ], 17 | }) 18 | export class HomeComponent {} 19 | -------------------------------------------------------------------------------- /libs/home/src/lib/home.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { SidebarComponent } from './sidebar/sidebar/sidebar.component'; 4 | import { RouterModule } from '@angular/router'; 5 | import { SidebarTagsComponent } from './sidebar/sidebar-tags/sidebar-tags.component'; 6 | import { SidebarAdvertisementComponent } from './sidebar/sidebar-advertisement/sidebar-advertisement.component'; 7 | import { SidebarSocialLinksComponent } from './sidebar/sidebar-social-links/sidebar-social-links.component'; 8 | import { FeaturedArticleComponent } from './articles/featured-article/featured-article.component'; 9 | import { ArticleCardModule } from '@devto/global-components'; 10 | import { ArticleContainerComponent } from './articles/article-container/article-container.component'; 11 | import { ArticleHeaderComponent } from './articles/article-header/article-header.component'; 12 | import { HomeComponent } from './home.component'; 13 | import { LetModule, PushModule } from '@rx-angular/template'; 14 | import { ListingsComponent } from './rightbar/listings/listings.component'; 15 | import { RightbarContainerComponent } from './rightbar/rightbar-container/rightbar-container.component'; 16 | import { TagArticleComponent } from './rightbar/tag-article/tag-article.component'; 17 | @NgModule({ 18 | declarations: [ 19 | SidebarComponent, 20 | SidebarTagsComponent, 21 | SidebarAdvertisementComponent, 22 | SidebarSocialLinksComponent, 23 | FeaturedArticleComponent, 24 | ArticleContainerComponent, 25 | ArticleHeaderComponent, 26 | HomeComponent, 27 | ListingsComponent, 28 | RightbarContainerComponent, 29 | TagArticleComponent, 30 | ], 31 | imports: [ 32 | LetModule, 33 | ArticleCardModule, 34 | PushModule, 35 | CommonModule, 36 | RouterModule.forChild([ 37 | { 38 | path: '', 39 | component: HomeComponent, 40 | }, 41 | ]), 42 | ], 43 | }) 44 | export class HomeModule {} 45 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/listings/listings.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ListingsComponent } from './listings.component'; 4 | 5 | describe('ListingsComponent', () => { 6 | let component: ListingsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ListingsComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ListingsComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/listings/listings.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ListingsStore } from '../services/listings.store'; 3 | 4 | @Component({ 5 | selector: 'app-listings', 6 | template: ` 7 | 8 | Listings 9 | 10 | See all 11 | 12 | 13 | 14 | 15 | 20 | {{ listing.title }} 21 | {{ listing.category }} 22 | 23 | Create a Listing 26 | 27 | loading 28 | loading 29 | `, 30 | styles: [ 31 | ` 32 | header { 33 | padding: 0.75rem 1rem; 34 | border-bottom: 1px solid #eef0f1; 35 | h3 { 36 | font-size: 1.25rem; 37 | font-weight: 700; 38 | } 39 | 40 | .see-all-link { 41 | font-weight: 500; 42 | font-size: 0.875rem; 43 | color: #3b49df; 44 | } 45 | } 46 | 47 | .listing-item { 48 | display: block; 49 | padding: 1rem; 50 | border-bottom: 1px solid #eef0f1; 51 | color: #202428; 52 | } 53 | 54 | .listing-type { 55 | color: #64707d; 56 | font-size: 0.875rem; 57 | padding-top: 0.25rem; 58 | } 59 | 60 | .create-listing { 61 | font-weight: 500; 62 | padding: 0.75rem; 63 | font-size: 0.875rem; 64 | text-align: center; 65 | color: #202428; 66 | display: block; 67 | } 68 | 69 | :host { 70 | background-color: #f9fafa; 71 | color: #202428; 72 | box-shadow: 0 0 0 1px rgba(8, 9, 10, 0.05); 73 | display: block; 74 | border-radius: 5px; 75 | } 76 | `, 77 | ], 78 | }) 79 | export class ListingsComponent { 80 | listing$ = this.listingStore.listing$; 81 | constructor(private listingStore: ListingsStore) {} 82 | } 83 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/rightbar-container/rightbar-container.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RightbarContainerComponent } from './rightbar-container.component'; 4 | 5 | describe('RightbarContainerComponent', () => { 6 | let component: RightbarContainerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [RightbarContainerComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(RightbarContainerComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/rightbar-container/rightbar-container.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { HOME_RIGHTBAR_TAGS } from '@devto/global-constants'; 3 | @Component({ 4 | selector: 'app-rightbar-container', 5 | template: ` `, 12 | styles: [ 13 | ` 14 | aside { 15 | display: grid; 16 | grid-row-gap: 1rem; 17 | } 18 | :host { 19 | display: block; 20 | } 21 | `, 22 | ], 23 | }) 24 | export class RightbarContainerComponent { 25 | asideTags = HOME_RIGHTBAR_TAGS; 26 | } 27 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/services/article-tags.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ArticleTagsStore } from './article-tags.store'; 4 | 5 | describe('ArticleTagsService', () => { 6 | let service: ArticleTagsStore; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ArticleTagsStore); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/services/article-tags.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Article } from '@devto/core/models'; 3 | import { ComponentStore, tapResponse } from '@ngrx/component-store'; 4 | import { Observable } from 'rxjs'; 5 | import { switchMap } from 'rxjs/operators'; 6 | import { ArticleApiService } from '@devto/global-services'; 7 | 8 | interface ArticlesState { 9 | articles: Article[]; 10 | } 11 | @Injectable({ 12 | providedIn: 'root', 13 | }) 14 | export class ArticleTagsStore extends ComponentStore { 15 | readonly articles$ = this.select((state) => state.articles); 16 | 17 | readonly setArticles = this.updater( 18 | (state: ArticlesState, articles: Article[]) => ({ 19 | ...state, 20 | articles: articles, 21 | }) 22 | ); 23 | 24 | readonly getArticles = this.effect( 25 | (params: Observable>) => 26 | params.pipe( 27 | switchMap((params) => 28 | this.articleApiS.getArticles({ ...params, top: 3 }).pipe( 29 | tapResponse( 30 | (articles) => this.setArticles(articles), 31 | (error) => this.logError(error) 32 | ) 33 | ) 34 | ) 35 | ) 36 | ); 37 | 38 | constructor(private articleApiS: ArticleApiService) { 39 | super({ articles: [] }); 40 | } 41 | 42 | logError(error: unknown) { 43 | console.error(error); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/services/listings-api.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ListingsApiService } from './listings-api.service'; 4 | 5 | describe('ListingsApiService', () => { 6 | let service: ListingsApiService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ListingsApiService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/services/listings-api.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { Listing } from '@devto/core/models'; 5 | import { environment } from '@devto/environments'; 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class ListingsApiService { 10 | constructor(private http: HttpClient) {} 11 | 12 | getListing(): Observable { 13 | return this.http.get( 14 | `${environment.baseApi}/listings?per_page=5 ` 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/services/listings.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ListingsStore } from './listings.store'; 4 | 5 | describe('ListingsStore', () => { 6 | let service: ListingsStore; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ListingsStore); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/services/listings.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ComponentStore, tapResponse } from '@ngrx/component-store'; 3 | import { Listing } from '@devto/core/models'; 4 | import { ListingsApiService } from './listings-api.service'; 5 | interface ListingState { 6 | listing: Listing[]; 7 | } 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class ListingsStore extends ComponentStore { 12 | readonly listing$ = this.select((state) => state.listing); 13 | readonly setlistings = this.updater( 14 | (state: ListingState, listing: Listing[]) => ({ 15 | ...state, 16 | listing: listing, 17 | }) 18 | ); 19 | readonly getlistings = this.effect(() => 20 | this.listingApiS.getListing().pipe( 21 | tapResponse( 22 | (listings) => this.setlistings(listings), 23 | (error) => this.logError(error) 24 | ) 25 | ) 26 | ); 27 | constructor(private listingApiS: ListingsApiService) { 28 | super({ listing: [] }); 29 | } 30 | 31 | logError(error: unknown) { 32 | console.error(error); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/tag-article/tag-article.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TagArticleComponent } from './tag-article.component'; 4 | 5 | describe('TagArticleComponent', () => { 6 | let component: TagArticleComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [TagArticleComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(TagArticleComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/tag-article/tag-article.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { ArticleTagsStore } from '../services/article-tags.store'; 3 | 4 | @Component({ 5 | selector: 'app-tag-article', 6 | template: ` 7 | 8 | #{{ tag }} 9 | 10 | 11 | 12 | 17 | {{ article.title }} 18 | {{ article.comments_count }} comments 19 | 20 | 21 | loading 22 | loading 23 | `, 24 | styles: [ 25 | ` 26 | header { 27 | padding: 0.75rem 1rem; 28 | border-bottom: 1px solid #eef0f1; 29 | h3 { 30 | font-size: 1.25rem; 31 | font-weight: 700; 32 | } 33 | 34 | .see-all-link { 35 | font-weight: 500; 36 | font-size: 0.875rem; 37 | color: #3b49df; 38 | } 39 | } 40 | 41 | .listing-item { 42 | display: block; 43 | padding: 1rem; 44 | border-bottom: 1px solid #eef0f1; 45 | color: #202428; 46 | } 47 | 48 | .listing-type { 49 | color: #64707d; 50 | font-size: 0.875rem; 51 | padding-top: 0.25rem; 52 | } 53 | 54 | .create-listing { 55 | font-weight: 500; 56 | padding: 0.75rem; 57 | font-size: 0.875rem; 58 | text-align: center; 59 | color: #202428; 60 | display: block; 61 | } 62 | 63 | :host { 64 | background-color: #f9fafa; 65 | color: #202428; 66 | box-shadow: 0 0 0 1px rgba(8, 9, 10, 0.05); 67 | display: block; 68 | border-radius: 5px; 69 | } 70 | `, 71 | ], 72 | viewProviders: [ArticleTagsStore], 73 | }) 74 | export class TagArticleComponent implements OnInit { 75 | @Input() tag = ''; 76 | article$ = this.articleStore.articles$; 77 | 78 | constructor(private articleStore: ArticleTagsStore) {} 79 | 80 | ngOnInit(): void { 81 | this.articleStore.getArticles({ tag: this.tag }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/services/tags-api.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpClientTestingModule, 3 | HttpTestingController, 4 | } from '@angular/common/http/testing'; 5 | import { TestBed } from '@angular/core/testing'; 6 | import { environment } from '@devto/environments'; 7 | 8 | import { TagsApiService } from './tags-api.service'; 9 | 10 | describe('TagsApiService', () => { 11 | let service: TagsApiService; 12 | let httpMock: HttpTestingController; 13 | 14 | beforeEach(() => { 15 | TestBed.configureTestingModule({ 16 | imports: [HttpClientTestingModule], 17 | }); 18 | service = TestBed.inject(TagsApiService); 19 | httpMock = TestBed.inject(HttpTestingController); 20 | }); 21 | 22 | afterEach(() => { 23 | // After every test, assert that there are no more pending requests. 24 | httpMock.verify(); 25 | }); 26 | 27 | it('should be created', () => { 28 | expect(service).toBeTruthy(); 29 | }); 30 | 31 | it('should send a get request to appropriate url on getTags subscription', () => { 32 | service.getTags().subscribe(); 33 | const req = httpMock.expectOne(`${environment.baseApi}/tags`); 34 | expect(req.request.method).toEqual('GET'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/services/tags-api.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { Tag } from '@devto/core/models'; 5 | import { environment } from '@devto/environments'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class TagsApiService { 11 | constructor(private http: HttpClient) {} 12 | 13 | getTags(): Observable { 14 | return this.http.get(`${environment.baseApi}/tags`); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/services/tags.store.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { of } from 'rxjs'; 4 | import { TagsApiService } from './tags-api.service'; 5 | 6 | import { TagsStore } from './tags.store'; 7 | 8 | const mockTags = [ 9 | { 10 | id: 6, 11 | name: 'javascript', 12 | bg_color_hex: '#F7DF1E', 13 | text_color_hex: '#000000', 14 | }, 15 | { 16 | id: 8, 17 | name: 'webdev', 18 | bg_color_hex: '#562765', 19 | text_color_hex: '#ffffff', 20 | }, 21 | ]; 22 | 23 | const mockTagsService = { 24 | getTags: () => of([mockTags[0]]), 25 | }; 26 | 27 | describe('TagsStore', () => { 28 | let store: TagsStore; 29 | let tagsApiService: TagsApiService; 30 | 31 | beforeEach(() => { 32 | TestBed.configureTestingModule({ 33 | imports: [HttpClientTestingModule], 34 | providers: [{ provide: TagsApiService, useValue: mockTagsService }], 35 | }); 36 | store = TestBed.inject(TagsStore); 37 | tagsApiService = TestBed.inject(TagsApiService); 38 | }); 39 | 40 | it('should be created', () => { 41 | expect(store).toBeTruthy(); 42 | }); 43 | 44 | it('should set initial state as in constructor', (done) => { 45 | store.state$.subscribe((tags) => { 46 | expect(tags).toEqual({ tags: [] }); 47 | done(); 48 | }); 49 | }); 50 | 51 | it('should change state as in setTags function', (done) => { 52 | store.setTags(mockTags); 53 | store.state$.subscribe((tags) => { 54 | expect(tags.tags.length).toBe(2); 55 | expect(tags.tags[1]).toEqual(mockTags[1]); 56 | done(); 57 | }); 58 | }); 59 | 60 | it('should return the current value of the store tags', (done) => { 61 | store.setState({ tags: mockTags }); 62 | store.tags$.subscribe((tags) => { 63 | expect(tags.length).toBe(2); 64 | expect(tags[1]).toEqual(mockTags[1]); 65 | done(); 66 | }); 67 | }); 68 | 69 | it('should call the updater if effect is started', (done) => { 70 | const spy = jest.spyOn(tagsApiService, 'getTags'); 71 | spy.mockReturnValue(of(mockTags)); 72 | store.tags$.subscribe((tags) => { 73 | expect(tags.length).toBe(2); 74 | expect(tags[1]).toEqual(mockTags[1]); 75 | done(); 76 | }); 77 | store.getTags(); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/services/tags.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ComponentStore, tapResponse } from '@ngrx/component-store'; 3 | import { Tag } from '@devto/core/models'; 4 | import { TagsApiService } from './tags-api.service'; 5 | 6 | interface TagsState { 7 | tags: Tag[]; 8 | } 9 | 10 | @Injectable({ 11 | providedIn: 'root', 12 | }) 13 | export class TagsStore extends ComponentStore { 14 | readonly tags$ = this.select((state) => state.tags); 15 | readonly getTags = this.effect(() => 16 | this.tagsApiS.getTags().pipe( 17 | tapResponse( 18 | (tags) => this.setTags(tags), 19 | (error) => this.logError(error) 20 | ) 21 | ) 22 | ); 23 | 24 | readonly setTags = this.updater((state: TagsState, tags: Tag[]) => ({ 25 | ...state, 26 | tags, 27 | })); 28 | 29 | constructor(private readonly tagsApiS: TagsApiService) { 30 | super({ tags: [] }); 31 | } 32 | 33 | logError(error: unknown) { 34 | console.error(error); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/sidebar-advertisement/__snapshots__/sidebar-advertisement.component.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SidebarAdvertisementComponent should render 1`] = ` 4 | 7 | 8 | 11 | 17 | 18 | 21 | 24 | New Champion-Brand DEV Wear is Here! 25 | 26 | 27 | 28 | 29 | `; 30 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/sidebar-advertisement/sidebar-advertisement.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SidebarAdvertisementComponent } from './sidebar-advertisement.component'; 4 | 5 | describe('SidebarAdvertisementComponent', () => { 6 | let component: SidebarAdvertisementComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [SidebarAdvertisementComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(SidebarAdvertisementComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | 25 | it('should render', () => { 26 | expect(fixture.nativeElement).toMatchSnapshot(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/sidebar-advertisement/sidebar-advertisement.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-sidebar-advertisement', 5 | template: ` 6 | 13 | New Champion-Brand DEV Wear is Here! 14 | 15 | `, 16 | styles: [ 17 | ` 18 | .ad-decription { 19 | text-align: center; 20 | line-height: 1.29em; 21 | } 22 | `, 23 | ], 24 | }) 25 | export class SidebarAdvertisementComponent {} 26 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/sidebar-social-links/sidebar-social-links.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SidebarSocialLinksComponent } from './sidebar-social-links.component'; 4 | 5 | describe('SidebarSocialLinksComponent', () => { 6 | let component: SidebarSocialLinksComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [SidebarSocialLinksComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(SidebarSocialLinksComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | 25 | it('should render', () => { 26 | expect(fixture.nativeElement).toMatchSnapshot(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/sidebar-social-links/sidebar-social-links.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-sidebar-social-links', 5 | templateUrl: './sidebar-social-links.component.html', 6 | styles: [ 7 | ` 8 | :host { 9 | padding: 1rem; 10 | margin-top: 1rem; 11 | display: block; 12 | } 13 | .social-link-icon { 14 | margin: 0 0.5rem; 15 | color: #64707d; 16 | svg, 17 | svg path { 18 | fill: currentColor; 19 | } 20 | } 21 | `, 22 | ], 23 | }) 24 | export class SidebarSocialLinksComponent {} 25 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/sidebar-tags/__snapshots__/sidebar-tags.component.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SidebarTagsComponent should render 1`] = ` 4 | 7 | 10 | 13 | 16 | My Tags 17 | 18 | 23 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | `; 41 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/sidebar-tags/sidebar-tags.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { LetModule } from '@rx-angular/template'; 3 | import { of } from 'rxjs'; 4 | import { TagsStore } from '../services/tags.store'; 5 | 6 | import { SidebarTagsComponent } from './sidebar-tags.component'; 7 | 8 | describe('SidebarTagsComponent', () => { 9 | let component: SidebarTagsComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async () => { 13 | await TestBed.configureTestingModule({ 14 | imports: [LetModule], 15 | declarations: [SidebarTagsComponent], 16 | providers: [{ provide: TagsStore, useValue: mockTagsStore }], 17 | }).compileComponents(); 18 | }); 19 | 20 | beforeEach(() => { 21 | fixture = TestBed.createComponent(SidebarTagsComponent); 22 | component = fixture.componentInstance; 23 | fixture.detectChanges(); 24 | }); 25 | 26 | it('should create', () => { 27 | expect(component).toBeTruthy(); 28 | }); 29 | 30 | it('should render', () => { 31 | expect(fixture.nativeElement).toMatchSnapshot(); 32 | }); 33 | }); 34 | 35 | const mockTagsStore = { 36 | tags: of([ 37 | { 38 | id: 6, 39 | name: 'javascript', 40 | bg_color_hex: '#F7DF1E', 41 | text_color_hex: '#000000', 42 | }, 43 | { 44 | id: 8, 45 | name: 'webdev', 46 | bg_color_hex: '#562765', 47 | text_color_hex: '#ffffff', 48 | }, 49 | ]), 50 | }; 51 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/sidebar-tags/sidebar-tags.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { TagsStore } from '../services/tags.store'; 3 | 4 | @Component({ 5 | selector: 'app-sidebar-tags', 6 | template: ` 7 | 8 | My Tags 9 | 14 | 21 | 24 | 25 | 26 | 27 | 32 | 33 | #{{ tag.name }} 36 | 37 | 38 | loading 39 | `, 40 | styles: [ 41 | ` 42 | .tags-title { 43 | font-size: 16px; 44 | font-weight: 700; 45 | color: #202428; 46 | } 47 | 48 | header { 49 | padding: 0.5rem; 50 | } 51 | 52 | .followed-tags { 53 | height: 42vh; 54 | overflow-y: auto; 55 | a { 56 | display: block; 57 | padding: 0.5rem; 58 | color: #202428; 59 | border-radius: 5px; 60 | width: 100%; 61 | &:hover, 62 | &:active { 63 | background-color: rgba(8, 9, 10, 0.05); 64 | color: #323ebe; 65 | } 66 | } 67 | } 68 | `, 69 | ], 70 | }) 71 | export class SidebarTagsComponent { 72 | tags$ = this.tagsStore.tags$; 73 | constructor(private tagsStore: TagsStore) {} 74 | } 75 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/sidebar/sidebar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 2 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | 5 | import { HomeModule } from '../../home.module'; 6 | import { SidebarAdvertisementComponent } from '../sidebar-advertisement/sidebar-advertisement.component'; 7 | import { SidebarSocialLinksComponent } from '../sidebar-social-links/sidebar-social-links.component'; 8 | import { SidebarTagsComponent } from '../sidebar-tags/sidebar-tags.component'; 9 | import { SidebarComponent } from './sidebar.component'; 10 | 11 | describe('SidebarComponent', () => { 12 | let component: SidebarComponent; 13 | let fixture: ComponentFixture; 14 | 15 | beforeEach(async () => { 16 | await TestBed.configureTestingModule({ 17 | declarations: [ 18 | SidebarComponent, 19 | SidebarAdvertisementComponent, 20 | SidebarTagsComponent, 21 | SidebarSocialLinksComponent, 22 | ], 23 | 24 | imports: [HomeModule, HttpClientTestingModule, RouterTestingModule], 25 | }).compileComponents(); 26 | }); 27 | 28 | beforeEach(() => { 29 | fixture = TestBed.createComponent(SidebarComponent); 30 | component = fixture.componentInstance; 31 | fixture.detectChanges(); 32 | }); 33 | 34 | it('should create', () => { 35 | expect(component).toBeTruthy(); 36 | }); 37 | 38 | it('should render', () => { 39 | expect(fixture.nativeElement).toMatchSnapshot(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/sidebar/sidebar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-sidebar', 5 | templateUrl: './sidebar.component.html', 6 | styles: [ 7 | ` 8 | :host { 9 | display: block; 10 | width: 240px; 11 | } 12 | a { 13 | width: 100%; 14 | padding: 0.5rem; 15 | display: inline-flex; 16 | align-items: center; 17 | color: #202428; 18 | border-radius: 5px; 19 | &:hover, 20 | &:active { 21 | background-color: rgba(8, 9, 10, 0.05); 22 | color: #323ebe; 23 | } 24 | } 25 | 26 | .sidebar-link-icon { 27 | margin-right: 0.5rem; 28 | vertical-align: middle; 29 | width: 1.5rem; 30 | height: 1.5rem; 31 | font-size: 1.25rem; 32 | } 33 | 34 | .more-link { 35 | padding-left: 2.5rem; 36 | font-size: 0.875rem; 37 | color: #64707d; 38 | } 39 | `, 40 | ], 41 | }) 42 | export class SidebarComponent {} 43 | -------------------------------------------------------------------------------- /libs/home/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /libs/home/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true 18 | }, 19 | "angularCompilerOptions": { 20 | "strictInjectionParameters": true, 21 | "strictInputAccessModifiers": true, 22 | "strictTemplates": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /libs/home/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"], 13 | "include": ["**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /libs/home/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/home/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../dist/out-tsc", 6 | "module": "commonjs", 7 | "types": [ 8 | "jest", 9 | "node" 10 | ] 11 | }, 12 | "files": [ 13 | "src/test-setup.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /libs/listings/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": ["devto", "app"], 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": ["devto", "app"], 25 | "style": "kebab-case" 26 | } 27 | ], 28 | "@typescript-eslint/no-non-null-assertion": "off" 29 | } 30 | }, 31 | { 32 | "files": ["*.html"], 33 | "extends": ["plugin:@nrwl/nx/angular-template"], 34 | "rules": {} 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /libs/listings/README.md: -------------------------------------------------------------------------------- 1 | # listings 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test listings` to execute the unit tests. 8 | -------------------------------------------------------------------------------- /libs/listings/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'listings', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | stringifyContentPathRegex: '\\.(html|svg)$', 9 | }, 10 | }, 11 | coverageDirectory: '../../coverage/libs/listings', 12 | transform: { 13 | '^.+\\.(ts|js|html)$': 'jest-preset-angular', 14 | }, 15 | snapshotSerializers: [ 16 | 'jest-preset-angular/build/serializers/no-ng-attributes', 17 | 'jest-preset-angular/build/serializers/ng-snapshot', 18 | 'jest-preset-angular/build/serializers/html-comment', 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /libs/listings/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/libs/listings", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/listings/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@devto/listings", 3 | "version": "0.0.1", 4 | "peerDependencies": { 5 | "@angular/common": "^12.1.0", 6 | "@angular/core": "^12.1.0" 7 | }, 8 | "dependencies": { 9 | "tslib": "^2.2.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /libs/listings/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/listings.module'; 2 | -------------------------------------------------------------------------------- /libs/listings/src/lib/contents/listing-card/listing-card.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ listing.title }} 5 | 6 | 7 | {{ listing.bumped_at | date: 'MMM d' }} 8 | 9 | 10 | 15 | #{{ tag }} 16 | 17 | 18 | 19 | 20 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 45 | 46 | 47 | 48 | {{ listing.author.name }} 49 | 50 | 51 | {{ listing.category }} 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /libs/listings/src/lib/contents/listing-card/listing-card.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ListingCardComponent } from './listing-card.component'; 4 | 5 | describe('ListingCardComponent', () => { 6 | let component: ListingCardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ListingCardComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ListingCardComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/listings/src/lib/contents/listing-card/listing-card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { AuthorListing } from '@devto/core/models'; 3 | 4 | @Component({ 5 | selector: 'app-listing-card', 6 | templateUrl: './listing-card.component.html', 7 | styleUrls: ['./listing-card.component.scss'], 8 | }) 9 | export class ListingCardComponent { 10 | @Input() listing!: AuthorListing; 11 | } 12 | -------------------------------------------------------------------------------- /libs/listings/src/lib/contents/listings-content/listings-content.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ListingsContentComponent } from './listings-content.component'; 4 | 5 | describe('ListingsContentComponent', () => { 6 | let component: ListingsContentComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ListingsContentComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ListingsContentComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/listings/src/lib/contents/listings-content/listings-content.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { map, tap } from 'rxjs/operators'; 4 | import { ListingsStore } from '../service/listings-store'; 5 | 6 | @Component({ 7 | selector: 'app-listings-content', 8 | template: ` 9 | 10 | 15 | 16 | 17 | Load More... 18 | 19 | `, 20 | styles: [ 21 | ` 22 | .listing-container { 23 | font-size: 16px; 24 | } 25 | .listings-cards { 26 | display: grid; 27 | grid-gap: 1rem; 28 | grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); 29 | margin-bottom: 1.5rem; 30 | } 31 | `, 32 | ], 33 | }) 34 | export class ListingsContentComponent implements OnInit { 35 | hasPagination = false; 36 | listingStore$ = this.listingsStore.listings$.pipe( 37 | tap((listing) => (this.hasPagination = listing?.length === 75)) 38 | ); 39 | 40 | constructor( 41 | private listingsStore: ListingsStore, 42 | private activatedRoute: ActivatedRoute 43 | ) {} 44 | 45 | ngOnInit(): void { 46 | this.listingsStore.getListings( 47 | this.activatedRoute.params.pipe(map((param) => param.category)) 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /libs/listings/src/lib/contents/masonary.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { MasonaryDirective } from './masonary.directive'; 2 | 3 | describe('MasonaryDirective', () => { 4 | it('should create an instance', () => { 5 | const directive = new MasonaryDirective(); 6 | expect(directive).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /libs/listings/src/lib/contents/masonary.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, OnDestroy, OnInit } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[appMasonary]', 5 | }) 6 | export class MasonaryDirective implements OnInit, OnDestroy { 7 | private observer = new MutationObserver(() => { 8 | this.cardElments.forEach((cardElement) => { 9 | this.resizeMasonryItem(cardElement as HTMLElement); 10 | }); 11 | }); 12 | 13 | constructor(private gridElement: ElementRef) {} 14 | 15 | get cardElments() { 16 | return Array.from(this.gridElement.nativeElement.children); 17 | } 18 | 19 | ngOnInit() { 20 | this.observer.observe(this.gridElement.nativeElement, { childList: true }); 21 | } 22 | 23 | //Reference:https://w3bits.com/css-grid-masonry/ 24 | resizeMasonryItem = (cardeElment: HTMLElement) => { 25 | /* Get the grid object, its row-gap, and the size of its implicit rows */ 26 | const grid = this.gridElement.nativeElement; 27 | const rowGap = parseInt( 28 | window.getComputedStyle(grid).getPropertyValue('grid-row-gap'), 29 | 10 30 | ); 31 | const rowHeight = 0; 32 | // eslint-disable @typescript-eslint/no-non-null-assertion 33 | const rowSpan = Math.ceil( 34 | (cardeElment.querySelector('.listing-card')!.getBoundingClientRect() 35 | .height + 36 | rowGap) / 37 | (rowHeight + rowGap) 38 | ); 39 | cardeElment.style.gridRowEnd = `span ${rowSpan}`; 40 | }; 41 | 42 | ngOnDestroy() { 43 | this.observer.disconnect(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /libs/listings/src/lib/contents/service/listings-api.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ListingsApiService } from './listings-api.service'; 4 | 5 | describe('ListingsApiService', () => { 6 | let service: ListingsApiService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ListingsApiService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /libs/listings/src/lib/contents/service/listings-api.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpParams } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { ListingCategory, ListingsReponse } from '@devto/core/models'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class ListingsApiService { 10 | constructor(private http: HttpClient) {} 11 | 12 | getListings(category?: ListingCategory): Observable { 13 | const categoryParam = new HttpParams() 14 | .set('category', category || '') 15 | .toString(); 16 | return this.http.get( 17 | `https://dev.to/search/listings?${categoryParam}&listing_search=&page=0&per_page=75&tag_boolean_mode=all` 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /libs/listings/src/lib/contents/service/listings-store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ComponentStore, tapResponse } from '@ngrx/component-store'; 3 | import { Observable } from 'rxjs'; 4 | import { switchMap } from 'rxjs/operators'; 5 | import { ListingCategory, Listings } from '@devto/core/models'; 6 | import { ListingsApiService } from './listings-api.service'; 7 | 8 | interface ListingsState { 9 | listings: Listings; 10 | } 11 | 12 | @Injectable({ 13 | providedIn: 'root', 14 | }) 15 | export class ListingsStore extends ComponentStore { 16 | readonly listings$: Observable = this.select((state) => { 17 | return state.listings; 18 | }); 19 | readonly addListings = this.updater((state, listings: Listings) => ({ 20 | listings: [...state.listings, ...listings], 21 | })); 22 | 23 | readonly loadListings = this.updater((_, listings: Listings) => ({ 24 | listings: [...listings], 25 | })); 26 | 27 | constructor(private listingsApiS: ListingsApiService) { 28 | super({ listings: [] }); 29 | } 30 | 31 | getListings = this.effect((activatedRoute$: Observable) => 32 | activatedRoute$.pipe( 33 | switchMap((categoryParam) => 34 | this.listingsApiS.getListings(categoryParam).pipe( 35 | tapResponse( 36 | (listings) => { 37 | return this.loadListings(listings.result); 38 | }, 39 | (error) => this.logError(error) 40 | ) 41 | ) 42 | ) 43 | ) 44 | ); 45 | 46 | logError(error: unknown) { 47 | console.error(error); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /libs/listings/src/lib/listings-header/listings-header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ListingsHeaderComponent } from './listings-header.component'; 4 | 5 | describe('ListingsHeaderComponent', () => { 6 | let component: ListingsHeaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ListingsHeaderComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ListingsHeaderComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/listings/src/lib/listings-header/listings-header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-listings-header', 5 | template: ` 6 | Listings 7 | 8 | 9 | 10 | Create 11 | 12 | 13 | Manage 14 | 15 | 16 | 17 | `, 18 | styles: [ 19 | ` 20 | li { 21 | list-style: none; 22 | } 23 | .listings-main-title { 24 | font-size: 1.875rem; 25 | margin: 0; 26 | } 27 | .listings-header-actions { 28 | margin: 0; 29 | gap: 0.5rem; 30 | } 31 | `, 32 | ], 33 | }) 34 | export class ListingsHeaderComponent {} 35 | -------------------------------------------------------------------------------- /libs/listings/src/lib/listings.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ListingsComponent } from './listings.component'; 4 | 5 | describe('ListingsComponent', () => { 6 | let component: ListingsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ListingsComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ListingsComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/listings/src/lib/listings.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-listings', 5 | template: ` 6 | 7 | 8 | 9 | 10 | 11 | `, 12 | styles: [ 13 | ` 14 | main { 15 | padding-top: 1rem; 16 | display: grid; 17 | grid-gap: 1rem; 18 | grid-template-columns: 240px 1fr; 19 | } 20 | `, 21 | ], 22 | }) 23 | export class ListingsComponent {} 24 | -------------------------------------------------------------------------------- /libs/listings/src/lib/listings.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ListingsComponent } from './listings.component'; 4 | import { RouterModule } from '@angular/router'; 5 | import { ListingsHeaderComponent } from './listings-header/listings-header.component'; 6 | import { ListingsSidenavComponent } from './sidenav/listings-sidenav/listings-sidenav.component'; 7 | import { ListingsContentComponent } from './contents/listings-content/listings-content.component'; 8 | import { MasonaryDirective } from './contents/masonary.directive'; 9 | import { LetModule, PushModule } from '@rx-angular/template'; 10 | import { ListingCardComponent } from './contents/listing-card/listing-card.component'; 11 | 12 | @NgModule({ 13 | declarations: [ 14 | ListingsComponent, 15 | ListingsHeaderComponent, 16 | ListingsSidenavComponent, 17 | ListingsContentComponent, 18 | MasonaryDirective, 19 | ListingCardComponent, 20 | ], 21 | imports: [ 22 | LetModule, 23 | PushModule, 24 | RouterModule.forChild([ 25 | { 26 | path: '', 27 | component: ListingsComponent, 28 | children: [ 29 | { 30 | path: ':category', 31 | component: ListingsContentComponent, 32 | }, 33 | { 34 | path: '', 35 | component: ListingsContentComponent, 36 | }, 37 | ], 38 | }, 39 | ]), 40 | CommonModule, 41 | ], 42 | }) 43 | export class ListingsModule {} 44 | -------------------------------------------------------------------------------- /libs/listings/src/lib/sidenav/listings-sidenav/listings-sidenav.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ListingsSidenavComponent } from './listings-sidenav.component'; 4 | 5 | describe('ListingsSidenavComponent', () => { 6 | let component: ListingsSidenavComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ListingsSidenavComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ListingsSidenavComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/listings/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /libs/listings/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true 18 | }, 19 | "angularCompilerOptions": { 20 | "strictInjectionParameters": true, 21 | "strictInputAccessModifiers": true, 22 | "strictTemplates": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /libs/listings/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"], 13 | "include": ["**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /libs/listings/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/listings/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../dist/out-tsc", 6 | "module": "commonjs", 7 | "types": [ 8 | "jest", 9 | "node" 10 | ] 11 | }, 12 | "files": [ 13 | "src/test-setup.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /libs/styles/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "devto", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "devto", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nrwl/nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /libs/styles/README.md: -------------------------------------------------------------------------------- 1 | # styles 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test styles` to execute the unit tests. 8 | -------------------------------------------------------------------------------- /libs/styles/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'styles', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | stringifyContentPathRegex: '\\.(html|svg)$', 9 | }, 10 | }, 11 | coverageDirectory: '../../coverage/libs/styles', 12 | transform: { 13 | '^.+\\.(ts|js|html)$': 'jest-preset-angular', 14 | }, 15 | snapshotSerializers: [ 16 | 'jest-preset-angular/build/serializers/no-ng-attributes', 17 | 'jest-preset-angular/build/serializers/ng-snapshot', 18 | 'jest-preset-angular/build/serializers/html-comment', 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /libs/styles/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/libs/styles", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/styles/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@devto/styles", 3 | "version": "0.0.1", 4 | "peerDependencies": { 5 | "@angular/common": "^12.1.1", 6 | "@angular/core": "^12.1.1" 7 | }, 8 | "dependencies": { 9 | "tslib": "^2.2.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /libs/styles/src/index.scss: -------------------------------------------------------------------------------- 1 | @import "./lib/global"; 2 | @import "./lib/article-detail"; 3 | @import "./lib/comments"; 4 | -------------------------------------------------------------------------------- /libs/styles/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/styles.module'; 2 | -------------------------------------------------------------------------------- /libs/styles/src/lib/_comments.scss: -------------------------------------------------------------------------------- 1 | app-comments { 2 | details { 3 | position: relative; 4 | } 5 | .comment-border { 6 | border-radius: 5px; 7 | background: #fff; 8 | color: rgb(8, 9, 10); 9 | box-shadow: rgba(8, 9, 10, 0.1) 0px 0px 0px 1px; 10 | overflow-wrap: anywhere; 11 | padding: 0.25rem; 12 | width: 100%; 13 | } 14 | 15 | summary.closed { 16 | display: block; 17 | align-items: center; 18 | transform: initial; 19 | top: 0; 20 | cursor: pointer; 21 | font-size: 0.875rem; 22 | color: #64707d; 23 | padding: 0.25rem 0.5rem; 24 | font-style: italic; 25 | border-radius: 5px; 26 | background: #f9fafa; 27 | margin-bottom: 1rem; 28 | } 29 | 30 | .comment-header { 31 | padding: 0.5rem 0.75rem 0 0.25rem; 32 | } 33 | 34 | .comment-username { 35 | padding: 0.5rem; 36 | } 37 | 38 | .comments-body { 39 | margin-bottom: 2rem; 40 | } 41 | 42 | .comment-inner-text { 43 | font-size: 1.125rem; 44 | padding: 0 0.75rem; 45 | margin: 0.5rem 0 1rem; 46 | p { 47 | margin-bottom: 1rem; 48 | } 49 | } 50 | 51 | .comment-avatar { 52 | width: 2rem; 53 | margin-right: 0.5rem; 54 | margin-top: 0.75rem; 55 | height: 2rem; 56 | img { 57 | border-radius: 100%; 58 | height: 100%; 59 | display: inline-block; 60 | vertical-align: bottom; 61 | } 62 | } 63 | .app-comments .app-comments .app-comments .app-comments { 64 | padding-left: 0; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /libs/styles/src/lib/styles.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | @NgModule({ 5 | imports: [CommonModule], 6 | }) 7 | export class StylesModule {} 8 | -------------------------------------------------------------------------------- /libs/styles/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /libs/styles/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true 18 | }, 19 | "angularCompilerOptions": { 20 | "strictInjectionParameters": true, 21 | "strictInputAccessModifiers": true, 22 | "strictTemplates": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /libs/styles/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"], 13 | "include": ["**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /libs/styles/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/styles/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../dist/out-tsc", 6 | "module": "commonjs", 7 | "types": [ 8 | "jest", 9 | "node" 10 | ] 11 | }, 12 | "files": [ 13 | "src/test-setup.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /libs/user-profile/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": ["devto","app"], 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": ["devto","app"], 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nrwl/nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /libs/user-profile/README.md: -------------------------------------------------------------------------------- 1 | # user-profile 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test user-profile` to execute the unit tests. 8 | -------------------------------------------------------------------------------- /libs/user-profile/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'user-profile', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | stringifyContentPathRegex: '\\.(html|svg)$', 9 | }, 10 | }, 11 | coverageDirectory: '../../coverage/libs/user-profile', 12 | transform: { 13 | '^.+\\.(ts|js|html)$': 'jest-preset-angular', 14 | }, 15 | snapshotSerializers: [ 16 | 'jest-preset-angular/build/serializers/no-ng-attributes', 17 | 'jest-preset-angular/build/serializers/ng-snapshot', 18 | 'jest-preset-angular/build/serializers/html-comment', 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /libs/user-profile/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/libs/user-profile", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/user-profile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@devto/user-profile", 3 | "version": "0.0.1", 4 | "peerDependencies": { 5 | "@angular/common": "^12.1.1", 6 | "@angular/core": "^12.1.1" 7 | }, 8 | "dependencies": { 9 | "tslib": "^2.2.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /libs/user-profile/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/user-profile.module'; 2 | -------------------------------------------------------------------------------- /libs/user-profile/src/lib/user-header/user-header.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | padding: 1rem; 4 | padding-top: 2rem; 5 | margin: 0 auto; 6 | max-width: 1024px; 7 | } 8 | 9 | header { 10 | border-radius: 5px; 11 | background: #fff; 12 | color: #08090a; 13 | box-shadow: 0 0 0 1px rgba(8, 9, 10, 0.1); 14 | overflow-wrap: anywhere; 15 | text-align: center; 16 | } 17 | 18 | .profile-header { 19 | position: relative; 20 | } 21 | 22 | img { 23 | padding: 0.5rem; 24 | width: 8rem; 25 | height: 8rem; 26 | border-radius: 100%; 27 | } 28 | 29 | .profile-header-details { 30 | padding: 1.5rem; 31 | h1 { 32 | font-weight: 800; 33 | font-size: 1.875rem; 34 | margin-bottom: 0.5rem; 35 | } 36 | 37 | .user-summary { 38 | font-size: 1.125rem; 39 | margin-bottom: 1rem; 40 | color: #202408; 41 | } 42 | } 43 | 44 | .profile-header-meta { 45 | font-size: 0.875rem; 46 | color: #64707d; 47 | margin-bottom: 0.5rem; 48 | display: flex; 49 | flex-wrap: wrap; 50 | align-items: center; 51 | justify-content: center; 52 | gap: 1.5rem; 53 | .item { 54 | display: flex; 55 | align-items: center; 56 | } 57 | svg { 58 | margin-right: 0.5rem; 59 | } 60 | } 61 | 62 | a { 63 | color: #64707d; 64 | } 65 | -------------------------------------------------------------------------------- /libs/user-profile/src/lib/user-header/user-header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UserHeaderComponent } from './user-header.component'; 4 | 5 | describe('UserHeaderComponent', () => { 6 | let component: UserHeaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [UserHeaderComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(UserHeaderComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/user-profile/src/lib/user-header/user-header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { UserDetails } from '@devto/core/models'; 3 | 4 | @Component({ 5 | selector: 'app-user-header', 6 | templateUrl: './user-header.component.html', 7 | styleUrls: ['./user-header.component.scss'], 8 | }) 9 | export class UserHeaderComponent { 10 | @Input() user!: UserDetails; 11 | } 12 | -------------------------------------------------------------------------------- /libs/user-profile/src/lib/user-profile.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UserProfileComponent } from './user-profile.component'; 4 | 5 | describe('UserProfileComponent', () => { 6 | let component: UserProfileComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [UserProfileComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(UserProfileComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/user-profile/src/lib/user-profile.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { UserStore } from '@devto/global-services'; 4 | import { UserArticlesStore } from '@devto/global-services'; 5 | 6 | @Component({ 7 | selector: 'app-user-profile', 8 | template: ` 9 | 10 | 11 | 12 | 13 | 14 | `, 15 | styles: [ 16 | ` 17 | app-article-card { 18 | margin-left: auto; 19 | margin-right: auto; 20 | max-width: 994px; 21 | } 22 | `, 23 | ], 24 | viewProviders: [UserStore, UserArticlesStore], 25 | }) 26 | export class UserProfileComponent implements OnInit { 27 | user$ = this.userStore.user$; 28 | articles$ = this.userArticles.articles$; 29 | constructor( 30 | private userStore: UserStore, 31 | private route: ActivatedRoute, 32 | private userArticles: UserArticlesStore 33 | ) {} 34 | ngOnInit() { 35 | this.userStore.getUser(this.route.snapshot.params.username); 36 | this.userArticles.getArticles({ 37 | username: this.route.snapshot.params.username, 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /libs/user-profile/src/lib/user-profile.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { UserProfileComponent } from './user-profile.component'; 4 | import { UserHeaderComponent } from './user-header/user-header.component'; 5 | import { RouterModule } from '@angular/router'; 6 | import { LetModule, PushModule } from '@rx-angular/template'; 7 | import { ArticleCardModule } from '@devto/global-components'; 8 | 9 | @NgModule({ 10 | declarations: [UserProfileComponent, UserHeaderComponent], 11 | imports: [ 12 | ArticleCardModule, 13 | LetModule, 14 | PushModule, 15 | CommonModule, 16 | RouterModule.forChild([ 17 | { 18 | path: '', 19 | component: UserProfileComponent, 20 | }, 21 | ]), 22 | ], 23 | }) 24 | export class UserProfileModule {} 25 | -------------------------------------------------------------------------------- /libs/user-profile/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /libs/user-profile/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true 18 | }, 19 | "angularCompilerOptions": { 20 | "strictInjectionParameters": true, 21 | "strictInputAccessModifiers": true, 22 | "strictTemplates": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /libs/user-profile/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"], 13 | "include": ["**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /libs/user-profile/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/user-profile/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../dist/out-tsc", 6 | "module": "commonjs", 7 | "types": [ 8 | "jest", 9 | "node" 10 | ] 11 | }, 12 | "files": [ 13 | "src/test-setup.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /libs/videos/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": ["devto","app"], 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": ["devto","app"], 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nrwl/nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /libs/videos/README.md: -------------------------------------------------------------------------------- 1 | # videos 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test videos` to execute the unit tests. 8 | -------------------------------------------------------------------------------- /libs/videos/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'videos', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | stringifyContentPathRegex: '\\.(html|svg)$', 9 | }, 10 | }, 11 | coverageDirectory: '../../coverage/libs/videos', 12 | transform: { 13 | '^.+\\.(ts|js|html)$': 'jest-preset-angular', 14 | }, 15 | snapshotSerializers: [ 16 | 'jest-preset-angular/build/serializers/no-ng-attributes', 17 | 'jest-preset-angular/build/serializers/ng-snapshot', 18 | 'jest-preset-angular/build/serializers/html-comment', 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /libs/videos/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/libs/videos", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/videos/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@devto/videos", 3 | "version": "0.0.1", 4 | "peerDependencies": { 5 | "@angular/common": "^12.1.1", 6 | "@angular/core": "^12.1.1" 7 | }, 8 | "dependencies": { 9 | "tslib": "^2.2.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /libs/videos/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/videos.module'; 2 | -------------------------------------------------------------------------------- /libs/videos/src/lib/videos-list/services/videos-list.store.spec.ts: -------------------------------------------------------------------------------- 1 | import { VideosListStore } from './videos-list.store'; 2 | 3 | describe('VideosListStore', () => { 4 | it('should create an instance', () => { 5 | expect(new VideosListStore()).toBeTruthy(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /libs/videos/src/lib/videos-list/services/videos-list.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ComponentStore, tapResponse } from '@ngrx/component-store'; 3 | import { Observable } from 'rxjs'; 4 | import { switchMap } from 'rxjs/operators'; 5 | import { VideosList } from '@devto/core/models'; 6 | import { VideoslistApiService } from './videoslist-api.service'; 7 | 8 | interface VideosListState { 9 | VideosList: VideosList[]; 10 | } 11 | 12 | @Injectable({ 13 | providedIn: 'root', 14 | }) 15 | export class VideosListStore extends ComponentStore { 16 | readonly VideosList$ = this.select((state) => state.VideosList); 17 | readonly setVideoslist = this.updater( 18 | (state: VideosListState, VideosList: VideosList[]) => ({ 19 | ...state, 20 | VideosList: [...state.VideosList, ...VideosList], 21 | }) 22 | ); 23 | 24 | readonly getVideoslist = this.effect( 25 | (params: Observable>) => 26 | params.pipe( 27 | switchMap((params) => 28 | this.VideoslistApiS.getVideoslist(params).pipe( 29 | tapResponse( 30 | (VideosList) => this.setVideoslist(VideosList), 31 | (error) => this.logError(error) 32 | ) 33 | ) 34 | ) 35 | ) 36 | ); 37 | constructor(private VideoslistApiS: VideoslistApiService) { 38 | super({ VideosList: [] }); 39 | } 40 | 41 | logError(error: unknown) { 42 | console.error(error); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /libs/videos/src/lib/videos-list/services/videoslist-api.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { VideoslistApiService } from './videoslist-api.service'; 4 | 5 | describe('VideoslistApiService', () => { 6 | let service: VideoslistApiService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(VideoslistApiService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /libs/videos/src/lib/videos-list/services/videoslist-api.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { VideosList } from '@devto/core/models'; 5 | import { environment } from '@devto/environments'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class VideoslistApiService { 11 | constructor(private http: HttpClient) {} 12 | 13 | getVideoslist(params?: Record): Observable { 14 | const newParams = new HttpParams({ fromObject: params }).toString(); 15 | return this.http.get( 16 | `${environment.baseApi}/videos?${newParams}` 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /libs/videos/src/lib/videos-list/video-card/video-card.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { VideoCardComponent } from './video-card.component'; 4 | 5 | describe('VideoCardComponent', () => { 6 | let component: VideoCardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [VideoCardComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(VideoCardComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/videos/src/lib/videos-list/video-card/video-card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { VideosList } from '@devto/core/models'; 3 | @Component({ 4 | selector: 'app-video-card', 5 | template: ` 6 | 7 | 13 | {{ 14 | video.video_duration_in_minutes 15 | }} 16 | 17 | 18 | {{ video.title }} 19 | 20 | {{ video.user.name }} 21 | 22 | `, 23 | styles: [ 24 | ` 25 | .video-collection { 26 | display: inline; 27 | 28 | .single-video-article { 29 | border: solid 1px #dbdbdb; 30 | margin: 5px; 31 | width: 100%; 32 | padding-bottom: 8px; 33 | border-radius: 3px; 34 | display: inline-block; 35 | .video-image { 36 | position: relative; 37 | padding-top: 56%; 38 | border-top-left-radius: 3px; 39 | border-top-right-radius: 3px; 40 | background: #0a0a0a no-repeat center center; 41 | -webkit-background-size: cover; 42 | -moz-background-size: cover; 43 | -o-background-size: cover; 44 | background-size: cover; 45 | } 46 | .video-timestamp { 47 | position: absolute; 48 | font-size: 0.7em; 49 | bottom: 11px; 50 | right: 4px; 51 | background-color: rgba(0, 0, 0, 0.8); 52 | color: #ffffff; 53 | padding: 2px 5px 3px; 54 | font-weight: 500; 55 | border-radius: 3px; 56 | } 57 | 58 | p { 59 | margin: 0px; 60 | padding: 2px 8px; 61 | max-height: 100%; 62 | max-width: 90%; 63 | overflow: hidden; 64 | white-space: nowrap; 65 | text-overflow: ellipsis; 66 | color: #08090a; 67 | font-size: 0.95em; 68 | } 69 | 70 | p.video-username { 71 | color: #64707d; 72 | font-size: 0.88em; 73 | } 74 | } 75 | } 76 | 77 | @media screen and (min-width: 550px) { 78 | .video-collection .single-video-article { 79 | width: 47%; 80 | } 81 | } 82 | @media screen and (min-width: 739px) { 83 | .video-collection .single-video-article { 84 | width: 31%; 85 | } 86 | } 87 | `, 88 | ], 89 | }) 90 | export class VideoCardComponent { 91 | @Input() video!: VideosList; 92 | } 93 | -------------------------------------------------------------------------------- /libs/videos/src/lib/videos-list/videos-header/videos-header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { VideosHeaderComponent } from './videos-header.component'; 4 | 5 | describe('VideosHeaderComponent', () => { 6 | let component: VideosHeaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [VideosHeaderComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(VideosHeaderComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/videos/src/lib/videos-list/videos-header/videos-header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-videos-header', 5 | template: ` 6 | DEV Community on Video 7 | `, 8 | styles: [ 9 | ` 10 | .video-page-title { 11 | font-size: calc(0.9vw + 10px); 12 | text-align: center; 13 | } 14 | `, 15 | ], 16 | }) 17 | export class VideosHeaderComponent {} 18 | -------------------------------------------------------------------------------- /libs/videos/src/lib/videos.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { VideosComponent } from './videos.component'; 4 | 5 | describe('VideosComponent', () => { 6 | let component: VideosComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [VideosComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(VideosComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/videos/src/lib/videos.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { VideosListStore } from './videos-list/services/videos-list.store'; 3 | 4 | @Component({ 5 | selector: 'app-videos', 6 | template: ` 7 | 8 | 9 | 10 | 11 | `, 12 | }) 13 | export class VideosComponent implements OnInit { 14 | page = '0'; 15 | videosList$ = this.VideosListStore.VideosList$; 16 | 17 | constructor(private VideosListStore: VideosListStore) {} 18 | 19 | ngOnInit(): void { 20 | this.VideosListStore.getVideoslist({ 21 | page: this.page, 22 | signature: '4072170', 23 | }); 24 | console.log(this.videosList$); 25 | } 26 | 27 | onScrollingFinished() { 28 | this.page = (Number(this.page) + 1).toString(); 29 | this.VideosListStore.getVideoslist({ 30 | page: this.page, 31 | signature: '4072170', 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /libs/videos/src/lib/videos.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterModule } from '@angular/router'; 4 | import { LetModule, PushModule } from '@rx-angular/template'; 5 | import { VideosComponent } from './videos.component'; 6 | import { VideosHeaderComponent } from './videos-list/videos-header/videos-header.component'; 7 | import { VideoCardComponent } from './videos-list/video-card/video-card.component'; 8 | import { ScrollTrackerDirective } from '@devto/global-components'; 9 | @NgModule({ 10 | declarations: [ 11 | VideosComponent, 12 | VideosHeaderComponent, 13 | VideoCardComponent, 14 | ScrollTrackerDirective, 15 | ], 16 | imports: [ 17 | LetModule, 18 | PushModule, 19 | CommonModule, 20 | RouterModule.forChild([ 21 | { 22 | path: '', 23 | component: VideosComponent, 24 | }, 25 | ]), 26 | ], 27 | }) 28 | export class VideosModule {} 29 | -------------------------------------------------------------------------------- /libs/videos/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /libs/videos/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true 18 | }, 19 | "angularCompilerOptions": { 20 | "strictInjectionParameters": true, 21 | "strictInputAccessModifiers": true, 22 | "strictTemplates": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /libs/videos/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"], 13 | "include": ["**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /libs/videos/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/videos/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../dist/out-tsc", 6 | "module": "commonjs", 7 | "types": [ 8 | "jest", 9 | "node" 10 | ] 11 | }, 12 | "files": [ 13 | "src/test-setup.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "devto", 3 | "affected": { 4 | "defaultBase": "main" 5 | }, 6 | "implicitDependencies": { 7 | "angular.json": "*", 8 | "package.json": "*", 9 | "tslint.json": "*", 10 | ".eslintrc.json": "*", 11 | "nx.json": "*", 12 | "tsconfig.base.json": "*" 13 | }, 14 | "projects": { 15 | "article-detail": { 16 | "tags": [] 17 | }, 18 | "core-app-routes": { 19 | "tags": [] 20 | }, 21 | "core-models": { 22 | "tags": [] 23 | }, 24 | "devto": { 25 | "tags": [], 26 | "implicitDependencies": ["styles"] 27 | }, 28 | "devto-e2e": { 29 | "tags": [], 30 | "implicitDependencies": ["devto"] 31 | }, 32 | "environments": { 33 | "tags": [] 34 | }, 35 | "global-components": { 36 | "tags": [] 37 | }, 38 | "global-constants": { 39 | "tags": [] 40 | }, 41 | "global-services": { 42 | "tags": [] 43 | }, 44 | "home": { 45 | "tags": [] 46 | }, 47 | "listings": { 48 | "tags": [] 49 | }, 50 | "styles": { 51 | "tags": [] 52 | }, 53 | "user-profile": { 54 | "tags": [] 55 | }, 56 | "videos": { 57 | "tags": [] 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tools/schematics/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajitsinghkaler/devto-clone/f8fa3fd2d220bc25bc6c19d420c15f3135b685d7/tools/schematics/.gitkeep -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"], 9 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "outDir": "./dist/out-tsc", 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "noImplicitReturns": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "sourceMap": true, 11 | "declaration": false, 12 | "downlevelIteration": true, 13 | "experimentalDecorators": true, 14 | "moduleResolution": "node", 15 | "importHelpers": true, 16 | "target": "es2017", 17 | "module": "es2020", 18 | "lib": ["es2018", "dom"], 19 | "paths": { 20 | "@devto/article-detail": ["libs/article-detail/src/index.ts"], 21 | "@devto/core/app-routes": ["libs/core/app-routes/src/index.ts"], 22 | "@devto/core/models": ["libs/core/models/src/index.ts"], 23 | "@devto/environments": ["libs/environments/src/index.ts"], 24 | "@devto/global-components": ["libs/global-components/src/index.ts"], 25 | "@devto/global-constants": ["libs/global-constants/src/index.ts"], 26 | "@devto/global-services": ["libs/global-services/src/index.ts"], 27 | "@devto/home": ["libs/home/src/index.ts"], 28 | "@devto/listings": ["libs/listings/src/index.ts"], 29 | "@devto/styles": ["libs/styles/src/index.ts"], 30 | "@devto/user-profile": ["libs/user-profile/src/index.ts"], 31 | "@devto/videos": ["libs/videos/src/index.ts"] 32 | }, 33 | "rootDir": "." 34 | }, 35 | "angularCompilerOptions": { 36 | "enableI18nLegacyMessageIdFormat": false, 37 | "strictInjectionParameters": true, 38 | "strictInputAccessModifiers": true, 39 | "strictTemplates": true 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "CommonJS" 5 | } 6 | } 7 | --------------------------------------------------------------------------------
( 18 | `${environment.baseApi}/articles?${newParams}` 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /libs/global-services/src/lib/dateago/dateago.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { DateagoPipe } from './dateago.pipe'; 2 | 3 | describe('DateagoPipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new DateagoPipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /libs/global-services/src/lib/dateago/dateago.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'dateago', 5 | }) 6 | export class DateagoPipe implements PipeTransform { 7 | transform(value: Date, args?: unknown[]): string { 8 | if (value) { 9 | const seconds = Math.floor((Date.now() - +new Date(value)) / 1000); 10 | if (seconds < 59) 11 | // less than 59 seconds ago will show as 'Just now' 12 | return 'Just now'; 13 | const intervals = { 14 | day: 86400, 15 | hour: 3600, 16 | minute: 60, 17 | }; 18 | switch (true) { 19 | case seconds < intervals.minute: { 20 | return '(Just Now)'; 21 | } 22 | case seconds < intervals.hour: { 23 | return `(${Math.floor(seconds / intervals.minute)} minute's ago)`; 24 | } 25 | case seconds < intervals.day: { 26 | return `(${Math.floor(seconds / intervals.hour)} + hours's ago`; 27 | } 28 | } 29 | } 30 | return ''; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /libs/global-services/src/lib/user-articles/user-articles.store.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { UserArticlesStore } from './user-articles.store'; 4 | 5 | describe('UserArticlesService', () => { 6 | let service: UserArticlesStore; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(UserArticlesStore); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /libs/global-services/src/lib/user-articles/user-articles.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Article } from '@devto/core/models'; 3 | import { ComponentStore, tapResponse } from '@ngrx/component-store'; 4 | import { Observable } from 'rxjs'; 5 | import { switchMap } from 'rxjs/operators'; 6 | import { ArticleApiService } from '@devto/global-services'; 7 | 8 | interface ArticlesState { 9 | articles: Article[]; 10 | } 11 | @Injectable({ 12 | providedIn: 'root', 13 | }) 14 | export class UserArticlesStore extends ComponentStore { 15 | readonly articles$ = this.select((state) => state.articles); 16 | 17 | readonly setArticles = this.updater( 18 | (state: ArticlesState, articles: Article[]) => { 19 | return { 20 | ...state, 21 | articles, 22 | }; 23 | } 24 | ); 25 | 26 | readonly getArticles = this.effect( 27 | (params: Observable>) => 28 | params.pipe( 29 | switchMap((params) => 30 | this.articleApiS.getArticles({ username: params.username }).pipe( 31 | tapResponse( 32 | (articles) => this.setArticles(articles), 33 | (error) => this.logError(error) 34 | ) 35 | ) 36 | ) 37 | ) 38 | ); 39 | 40 | constructor(private articleApiS: ArticleApiService) { 41 | super({ articles: [] }); 42 | } 43 | 44 | logError(error: unknown) { 45 | console.error(error); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /libs/global-services/src/lib/user/user-api.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { UserApiService } from './user-api.service'; 4 | 5 | describe('UserApiService', () => { 6 | let service: UserApiService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(UserApiService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /libs/global-services/src/lib/user/user-api.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { UserDetails } from '@devto/core/models'; 5 | import { environment } from '@devto/environments'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class UserApiService { 11 | constructor(private http: HttpClient) {} 12 | 13 | getUser(username?: string): Observable { 14 | return this.http.get( 15 | `${environment.baseApi}/users/by_username?url=${username}` 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /libs/global-services/src/lib/user/user.store.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { UserStore } from './user.store'; 4 | 5 | describe('UserService', () => { 6 | let service: UserStore; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(UserStore); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /libs/global-services/src/lib/user/user.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ComponentStore, tapResponse } from '@ngrx/component-store'; 3 | import { Observable } from 'rxjs'; 4 | import { switchMap } from 'rxjs/operators'; 5 | import { UserDetails } from '@devto/core/models'; 6 | import { UserApiService } from './user-api.service'; 7 | 8 | interface UserState { 9 | user: UserDetails | null; 10 | } 11 | 12 | @Injectable() 13 | export class UserStore extends ComponentStore { 14 | readonly user$ = this.select((state) => state.user); 15 | readonly setUser = this.updater((state: UserState, user: UserDetails) => ({ 16 | ...state, 17 | user, 18 | })); 19 | readonly getUser = this.effect((username: Observable) => 20 | username.pipe( 21 | switchMap((username) => 22 | this.userApiS.getUser(username).pipe( 23 | tapResponse( 24 | (user) => this.setUser(user), 25 | (error) => this.logError(error) 26 | ) 27 | ) 28 | ) 29 | ) 30 | ); 31 | 32 | constructor(private userApiS: UserApiService) { 33 | super({ user: null }); 34 | } 35 | 36 | private logError(error: unknown) { 37 | console.error(error); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /libs/global-services/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /libs/global-services/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true 18 | }, 19 | "angularCompilerOptions": { 20 | "strictInjectionParameters": true, 21 | "strictInputAccessModifiers": true, 22 | "strictTemplates": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /libs/global-services/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"], 13 | "include": ["**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /libs/global-services/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/global-services/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../dist/out-tsc", 6 | "module": "commonjs", 7 | "types": [ 8 | "jest", 9 | "node" 10 | ] 11 | }, 12 | "files": [ 13 | "src/test-setup.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /libs/home/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": ["devto","app"], 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": ["devto","app"], 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nrwl/nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /libs/home/README.md: -------------------------------------------------------------------------------- 1 | # home 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test home` to execute the unit tests. 8 | -------------------------------------------------------------------------------- /libs/home/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'home', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | stringifyContentPathRegex: '\\.(html|svg)$', 9 | }, 10 | }, 11 | coverageDirectory: '../../coverage/libs/home', 12 | transform: { 13 | '^.+\\.(ts|js|html)$': 'jest-preset-angular', 14 | }, 15 | snapshotSerializers: [ 16 | 'jest-preset-angular/build/serializers/no-ng-attributes', 17 | 'jest-preset-angular/build/serializers/ng-snapshot', 18 | 'jest-preset-angular/build/serializers/html-comment', 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /libs/home/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/libs/home", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/home/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@devto/home", 3 | "version": "0.0.1", 4 | "peerDependencies": { 5 | "@angular/common": "^12.1.1", 6 | "@angular/core": "^12.1.1" 7 | }, 8 | "dependencies": { 9 | "tslib": "^2.2.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /libs/home/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/home.module'; 2 | -------------------------------------------------------------------------------- /libs/home/src/lib/articles/article-container/article-container.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ArticleContainerComponent } from './article-container.component'; 4 | 5 | describe('ArticleContainerComponent', () => { 6 | let component: ArticleContainerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ArticleContainerComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ArticleContainerComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/home/src/lib/articles/article-container/article-container.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ArticleStore } from '../services/article.store'; 3 | 4 | @Component({ 5 | selector: 'app-article-container', 6 | template: ` 7 | 10 | 11 | 12 | `, 13 | }) 14 | export class ArticleContainerComponent { 15 | articles$ = this.articleStore.articles$; 16 | featuredArticle$ = this.articleStore.featuredArticle$; 17 | 18 | constructor(private articleStore: ArticleStore) {} 19 | } 20 | -------------------------------------------------------------------------------- /libs/home/src/lib/articles/article-header/article-header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ArticleHeaderComponent } from './article-header.component'; 4 | 5 | describe('ArticleHeaderComponent', () => { 6 | let component: ArticleHeaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ArticleHeaderComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ArticleHeaderComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/home/src/lib/articles/article-header/article-header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ARTICLE_HEADER_TABS } from '@devto/global-constants'; 3 | 4 | @Component({ 5 | selector: 'app-article-header', 6 | template: ` 7 | Posts 8 | 9 | 10 | 11 | 12 | {{ 13 | tab | titlecase 14 | }} 15 | 16 | 17 | 18 | `, 19 | styles: [ 20 | ` 21 | .tabs-list { 22 | margin: 0; 23 | padding: 0; 24 | } 25 | 26 | .tab-item { 27 | padding: 0.5rem; 28 | margin: 0 0.25rem; 29 | flex-direction: column; 30 | font-size: 1rem; 31 | transition: all cubic-bezier(0.17, 0.67, 0.5, 0.71) 100ms; 32 | border-radius: 5px; 33 | &.active { 34 | font-weight: 500; 35 | color: #08090a; 36 | } 37 | &:before { 38 | content: attr(data-text); 39 | height: 0; 40 | visibility: hidden; 41 | overflow: hidden; 42 | user-select: none; 43 | pointer-events: none; 44 | } 45 | &:after { 46 | position: absolute; 47 | content: ''; 48 | } 49 | } 50 | `, 51 | ], 52 | }) 53 | export class ArticleHeaderComponent { 54 | selectedTab = 'feed'; 55 | tabs = ARTICLE_HEADER_TABS; 56 | } 57 | -------------------------------------------------------------------------------- /libs/home/src/lib/articles/featured-article/featured-article.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FeaturedArticleComponent } from './featured-article.component'; 4 | 5 | describe('FeaturedArticleComponent', () => { 6 | let component: FeaturedArticleComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [FeaturedArticleComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(FeaturedArticleComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/home/src/lib/articles/featured-article/featured-article.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { Article } from '@devto/core/models'; 3 | 4 | @Component({ 5 | selector: 'app-featured-article', 6 | template: ` 13 | 14 | 15 | 21 | {{ featured?.title }} 22 | 23 | `, 24 | styles: [ 25 | ` 26 | :host { 27 | display: block; 28 | background-color: #fff; 29 | box-shadow: 0 0 0 1px rgba(8, 9, 10, 0.1); 30 | border-radius: 5px; 31 | cursor: pointer; 32 | margin: 0 0 0.75rem; 33 | overflow: hidden; 34 | &:focus-within { 35 | outline: none; 36 | box-shadow: 0 0 0 2px #3b49df; 37 | } 38 | } 39 | 40 | app-article-card { 41 | box-shadow: none; 42 | margin: 0 0 0; 43 | &:focus-within { 44 | outline: none; 45 | box-shadow: none; 46 | } 47 | } 48 | 49 | .featured-img { 50 | display: block; 51 | width: 100%; 52 | height: auto; 53 | padding-bottom: 42%; 54 | background-size: cover; 55 | background-position: center center; 56 | } 57 | 58 | .featured-title { 59 | font-size: 1.875rem; 60 | } 61 | `, 62 | ], 63 | }) 64 | export class FeaturedArticleComponent { 65 | @Input() featured!: Article; 66 | } 67 | -------------------------------------------------------------------------------- /libs/home/src/lib/articles/services/article.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ArticleStore } from './article.store'; 4 | 5 | describe('ArticleStore', () => { 6 | let service: ArticleStore; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ArticleStore); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /libs/home/src/lib/articles/services/article.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ComponentStore, tapResponse } from '@ngrx/component-store'; 3 | import { ArticleApiService } from '@devto/global-services'; 4 | import { Article } from '@devto/core/models'; 5 | 6 | interface ArticlesState { 7 | articles: Article[]; 8 | featured?: Article; 9 | } 10 | 11 | @Injectable({ 12 | providedIn: 'root', 13 | }) 14 | export class ArticleStore extends ComponentStore { 15 | readonly articles$ = this.select((state) => state.articles); 16 | readonly featuredArticle$ = this.select((state) => state.featured); 17 | readonly setArticles = this.updater( 18 | (state: ArticlesState, articles: Article[]) => { 19 | let index = 0; 20 | articles.find((article, idx) => { 21 | index = idx; 22 | return article.cover_image; 23 | }); 24 | let featured = articles.splice(index, 1)[0]; 25 | return { 26 | ...state, 27 | featured: featured, 28 | articles: articles, 29 | }; 30 | } 31 | ); 32 | readonly getArticles = this.effect(() => 33 | this.articleApiS.getArticles({ state: 'rising' }).pipe( 34 | tapResponse( 35 | (articles) => this.setArticles(articles), 36 | (error) => this.logError(error) 37 | ) 38 | ) 39 | ); 40 | constructor(private articleApiS: ArticleApiService) { 41 | super({ articles: [] }); 42 | } 43 | 44 | logError(error: unknown) { 45 | console.error(error); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /libs/home/src/lib/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { RouterTestingModule } from "@angular/router/testing"; 4 | 5 | import { ArticleContainerComponent } from './articles/article-container/article-container.component'; 6 | import { HomeComponent } from './home.component'; 7 | import { RightbarContainerComponent } from './rightbar/rightbar-container/rightbar-container.component'; 8 | import { SidebarComponent } from './sidebar/sidebar/sidebar.component'; 9 | import { HomeModule } from './home.module'; 10 | 11 | describe('HomeComponent', () => { 12 | let component: HomeComponent; 13 | let fixture: ComponentFixture; 14 | 15 | beforeEach(async () => { 16 | await TestBed.configureTestingModule({ 17 | declarations: [ 18 | HomeComponent, 19 | SidebarComponent, 20 | ArticleContainerComponent, 21 | RightbarContainerComponent, 22 | ], 23 | imports: [HomeModule, HttpClientTestingModule, RouterTestingModule], 24 | }).compileComponents(); 25 | }); 26 | 27 | beforeEach(() => { 28 | fixture = TestBed.createComponent(HomeComponent); 29 | component = fixture.componentInstance; 30 | fixture.detectChanges(); 31 | }); 32 | 33 | it('should create', () => { 34 | expect(component).toBeTruthy(); 35 | }); 36 | 37 | it('should render', () => { 38 | expect(fixture.nativeElement).toMatchSnapshot(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /libs/home/src/lib/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-home', 5 | template: ` 6 | 7 | `, 8 | styles: [ 9 | ` 10 | :host { 11 | display: grid; 12 | grid-gap: 1rem; 13 | grid-template-columns: 240px 2fr 1fr; 14 | } 15 | `, 16 | ], 17 | }) 18 | export class HomeComponent {} 19 | -------------------------------------------------------------------------------- /libs/home/src/lib/home.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { SidebarComponent } from './sidebar/sidebar/sidebar.component'; 4 | import { RouterModule } from '@angular/router'; 5 | import { SidebarTagsComponent } from './sidebar/sidebar-tags/sidebar-tags.component'; 6 | import { SidebarAdvertisementComponent } from './sidebar/sidebar-advertisement/sidebar-advertisement.component'; 7 | import { SidebarSocialLinksComponent } from './sidebar/sidebar-social-links/sidebar-social-links.component'; 8 | import { FeaturedArticleComponent } from './articles/featured-article/featured-article.component'; 9 | import { ArticleCardModule } from '@devto/global-components'; 10 | import { ArticleContainerComponent } from './articles/article-container/article-container.component'; 11 | import { ArticleHeaderComponent } from './articles/article-header/article-header.component'; 12 | import { HomeComponent } from './home.component'; 13 | import { LetModule, PushModule } from '@rx-angular/template'; 14 | import { ListingsComponent } from './rightbar/listings/listings.component'; 15 | import { RightbarContainerComponent } from './rightbar/rightbar-container/rightbar-container.component'; 16 | import { TagArticleComponent } from './rightbar/tag-article/tag-article.component'; 17 | @NgModule({ 18 | declarations: [ 19 | SidebarComponent, 20 | SidebarTagsComponent, 21 | SidebarAdvertisementComponent, 22 | SidebarSocialLinksComponent, 23 | FeaturedArticleComponent, 24 | ArticleContainerComponent, 25 | ArticleHeaderComponent, 26 | HomeComponent, 27 | ListingsComponent, 28 | RightbarContainerComponent, 29 | TagArticleComponent, 30 | ], 31 | imports: [ 32 | LetModule, 33 | ArticleCardModule, 34 | PushModule, 35 | CommonModule, 36 | RouterModule.forChild([ 37 | { 38 | path: '', 39 | component: HomeComponent, 40 | }, 41 | ]), 42 | ], 43 | }) 44 | export class HomeModule {} 45 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/listings/listings.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ListingsComponent } from './listings.component'; 4 | 5 | describe('ListingsComponent', () => { 6 | let component: ListingsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ListingsComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ListingsComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/listings/listings.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ListingsStore } from '../services/listings.store'; 3 | 4 | @Component({ 5 | selector: 'app-listings', 6 | template: ` 7 | 8 | Listings 9 | 10 | See all 11 | 12 | 13 | 14 | 15 | 20 | {{ listing.title }} 21 | {{ listing.category }} 22 | 23 | Create a Listing 26 | 27 | loading 28 | loading 29 | `, 30 | styles: [ 31 | ` 32 | header { 33 | padding: 0.75rem 1rem; 34 | border-bottom: 1px solid #eef0f1; 35 | h3 { 36 | font-size: 1.25rem; 37 | font-weight: 700; 38 | } 39 | 40 | .see-all-link { 41 | font-weight: 500; 42 | font-size: 0.875rem; 43 | color: #3b49df; 44 | } 45 | } 46 | 47 | .listing-item { 48 | display: block; 49 | padding: 1rem; 50 | border-bottom: 1px solid #eef0f1; 51 | color: #202428; 52 | } 53 | 54 | .listing-type { 55 | color: #64707d; 56 | font-size: 0.875rem; 57 | padding-top: 0.25rem; 58 | } 59 | 60 | .create-listing { 61 | font-weight: 500; 62 | padding: 0.75rem; 63 | font-size: 0.875rem; 64 | text-align: center; 65 | color: #202428; 66 | display: block; 67 | } 68 | 69 | :host { 70 | background-color: #f9fafa; 71 | color: #202428; 72 | box-shadow: 0 0 0 1px rgba(8, 9, 10, 0.05); 73 | display: block; 74 | border-radius: 5px; 75 | } 76 | `, 77 | ], 78 | }) 79 | export class ListingsComponent { 80 | listing$ = this.listingStore.listing$; 81 | constructor(private listingStore: ListingsStore) {} 82 | } 83 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/rightbar-container/rightbar-container.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RightbarContainerComponent } from './rightbar-container.component'; 4 | 5 | describe('RightbarContainerComponent', () => { 6 | let component: RightbarContainerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [RightbarContainerComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(RightbarContainerComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/rightbar-container/rightbar-container.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { HOME_RIGHTBAR_TAGS } from '@devto/global-constants'; 3 | @Component({ 4 | selector: 'app-rightbar-container', 5 | template: ` `, 12 | styles: [ 13 | ` 14 | aside { 15 | display: grid; 16 | grid-row-gap: 1rem; 17 | } 18 | :host { 19 | display: block; 20 | } 21 | `, 22 | ], 23 | }) 24 | export class RightbarContainerComponent { 25 | asideTags = HOME_RIGHTBAR_TAGS; 26 | } 27 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/services/article-tags.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ArticleTagsStore } from './article-tags.store'; 4 | 5 | describe('ArticleTagsService', () => { 6 | let service: ArticleTagsStore; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ArticleTagsStore); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/services/article-tags.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Article } from '@devto/core/models'; 3 | import { ComponentStore, tapResponse } from '@ngrx/component-store'; 4 | import { Observable } from 'rxjs'; 5 | import { switchMap } from 'rxjs/operators'; 6 | import { ArticleApiService } from '@devto/global-services'; 7 | 8 | interface ArticlesState { 9 | articles: Article[]; 10 | } 11 | @Injectable({ 12 | providedIn: 'root', 13 | }) 14 | export class ArticleTagsStore extends ComponentStore { 15 | readonly articles$ = this.select((state) => state.articles); 16 | 17 | readonly setArticles = this.updater( 18 | (state: ArticlesState, articles: Article[]) => ({ 19 | ...state, 20 | articles: articles, 21 | }) 22 | ); 23 | 24 | readonly getArticles = this.effect( 25 | (params: Observable>) => 26 | params.pipe( 27 | switchMap((params) => 28 | this.articleApiS.getArticles({ ...params, top: 3 }).pipe( 29 | tapResponse( 30 | (articles) => this.setArticles(articles), 31 | (error) => this.logError(error) 32 | ) 33 | ) 34 | ) 35 | ) 36 | ); 37 | 38 | constructor(private articleApiS: ArticleApiService) { 39 | super({ articles: [] }); 40 | } 41 | 42 | logError(error: unknown) { 43 | console.error(error); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/services/listings-api.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ListingsApiService } from './listings-api.service'; 4 | 5 | describe('ListingsApiService', () => { 6 | let service: ListingsApiService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ListingsApiService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/services/listings-api.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { Listing } from '@devto/core/models'; 5 | import { environment } from '@devto/environments'; 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class ListingsApiService { 10 | constructor(private http: HttpClient) {} 11 | 12 | getListing(): Observable { 13 | return this.http.get( 14 | `${environment.baseApi}/listings?per_page=5 ` 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/services/listings.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ListingsStore } from './listings.store'; 4 | 5 | describe('ListingsStore', () => { 6 | let service: ListingsStore; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ListingsStore); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/services/listings.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ComponentStore, tapResponse } from '@ngrx/component-store'; 3 | import { Listing } from '@devto/core/models'; 4 | import { ListingsApiService } from './listings-api.service'; 5 | interface ListingState { 6 | listing: Listing[]; 7 | } 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class ListingsStore extends ComponentStore { 12 | readonly listing$ = this.select((state) => state.listing); 13 | readonly setlistings = this.updater( 14 | (state: ListingState, listing: Listing[]) => ({ 15 | ...state, 16 | listing: listing, 17 | }) 18 | ); 19 | readonly getlistings = this.effect(() => 20 | this.listingApiS.getListing().pipe( 21 | tapResponse( 22 | (listings) => this.setlistings(listings), 23 | (error) => this.logError(error) 24 | ) 25 | ) 26 | ); 27 | constructor(private listingApiS: ListingsApiService) { 28 | super({ listing: [] }); 29 | } 30 | 31 | logError(error: unknown) { 32 | console.error(error); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/tag-article/tag-article.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TagArticleComponent } from './tag-article.component'; 4 | 5 | describe('TagArticleComponent', () => { 6 | let component: TagArticleComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [TagArticleComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(TagArticleComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/home/src/lib/rightbar/tag-article/tag-article.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { ArticleTagsStore } from '../services/article-tags.store'; 3 | 4 | @Component({ 5 | selector: 'app-tag-article', 6 | template: ` 7 | 8 | #{{ tag }} 9 | 10 | 11 | 12 | 17 | {{ article.title }} 18 | {{ article.comments_count }} comments 19 | 20 | 21 | loading 22 | loading 23 | `, 24 | styles: [ 25 | ` 26 | header { 27 | padding: 0.75rem 1rem; 28 | border-bottom: 1px solid #eef0f1; 29 | h3 { 30 | font-size: 1.25rem; 31 | font-weight: 700; 32 | } 33 | 34 | .see-all-link { 35 | font-weight: 500; 36 | font-size: 0.875rem; 37 | color: #3b49df; 38 | } 39 | } 40 | 41 | .listing-item { 42 | display: block; 43 | padding: 1rem; 44 | border-bottom: 1px solid #eef0f1; 45 | color: #202428; 46 | } 47 | 48 | .listing-type { 49 | color: #64707d; 50 | font-size: 0.875rem; 51 | padding-top: 0.25rem; 52 | } 53 | 54 | .create-listing { 55 | font-weight: 500; 56 | padding: 0.75rem; 57 | font-size: 0.875rem; 58 | text-align: center; 59 | color: #202428; 60 | display: block; 61 | } 62 | 63 | :host { 64 | background-color: #f9fafa; 65 | color: #202428; 66 | box-shadow: 0 0 0 1px rgba(8, 9, 10, 0.05); 67 | display: block; 68 | border-radius: 5px; 69 | } 70 | `, 71 | ], 72 | viewProviders: [ArticleTagsStore], 73 | }) 74 | export class TagArticleComponent implements OnInit { 75 | @Input() tag = ''; 76 | article$ = this.articleStore.articles$; 77 | 78 | constructor(private articleStore: ArticleTagsStore) {} 79 | 80 | ngOnInit(): void { 81 | this.articleStore.getArticles({ tag: this.tag }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/services/tags-api.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpClientTestingModule, 3 | HttpTestingController, 4 | } from '@angular/common/http/testing'; 5 | import { TestBed } from '@angular/core/testing'; 6 | import { environment } from '@devto/environments'; 7 | 8 | import { TagsApiService } from './tags-api.service'; 9 | 10 | describe('TagsApiService', () => { 11 | let service: TagsApiService; 12 | let httpMock: HttpTestingController; 13 | 14 | beforeEach(() => { 15 | TestBed.configureTestingModule({ 16 | imports: [HttpClientTestingModule], 17 | }); 18 | service = TestBed.inject(TagsApiService); 19 | httpMock = TestBed.inject(HttpTestingController); 20 | }); 21 | 22 | afterEach(() => { 23 | // After every test, assert that there are no more pending requests. 24 | httpMock.verify(); 25 | }); 26 | 27 | it('should be created', () => { 28 | expect(service).toBeTruthy(); 29 | }); 30 | 31 | it('should send a get request to appropriate url on getTags subscription', () => { 32 | service.getTags().subscribe(); 33 | const req = httpMock.expectOne(`${environment.baseApi}/tags`); 34 | expect(req.request.method).toEqual('GET'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/services/tags-api.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { Tag } from '@devto/core/models'; 5 | import { environment } from '@devto/environments'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class TagsApiService { 11 | constructor(private http: HttpClient) {} 12 | 13 | getTags(): Observable { 14 | return this.http.get(`${environment.baseApi}/tags`); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/services/tags.store.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { of } from 'rxjs'; 4 | import { TagsApiService } from './tags-api.service'; 5 | 6 | import { TagsStore } from './tags.store'; 7 | 8 | const mockTags = [ 9 | { 10 | id: 6, 11 | name: 'javascript', 12 | bg_color_hex: '#F7DF1E', 13 | text_color_hex: '#000000', 14 | }, 15 | { 16 | id: 8, 17 | name: 'webdev', 18 | bg_color_hex: '#562765', 19 | text_color_hex: '#ffffff', 20 | }, 21 | ]; 22 | 23 | const mockTagsService = { 24 | getTags: () => of([mockTags[0]]), 25 | }; 26 | 27 | describe('TagsStore', () => { 28 | let store: TagsStore; 29 | let tagsApiService: TagsApiService; 30 | 31 | beforeEach(() => { 32 | TestBed.configureTestingModule({ 33 | imports: [HttpClientTestingModule], 34 | providers: [{ provide: TagsApiService, useValue: mockTagsService }], 35 | }); 36 | store = TestBed.inject(TagsStore); 37 | tagsApiService = TestBed.inject(TagsApiService); 38 | }); 39 | 40 | it('should be created', () => { 41 | expect(store).toBeTruthy(); 42 | }); 43 | 44 | it('should set initial state as in constructor', (done) => { 45 | store.state$.subscribe((tags) => { 46 | expect(tags).toEqual({ tags: [] }); 47 | done(); 48 | }); 49 | }); 50 | 51 | it('should change state as in setTags function', (done) => { 52 | store.setTags(mockTags); 53 | store.state$.subscribe((tags) => { 54 | expect(tags.tags.length).toBe(2); 55 | expect(tags.tags[1]).toEqual(mockTags[1]); 56 | done(); 57 | }); 58 | }); 59 | 60 | it('should return the current value of the store tags', (done) => { 61 | store.setState({ tags: mockTags }); 62 | store.tags$.subscribe((tags) => { 63 | expect(tags.length).toBe(2); 64 | expect(tags[1]).toEqual(mockTags[1]); 65 | done(); 66 | }); 67 | }); 68 | 69 | it('should call the updater if effect is started', (done) => { 70 | const spy = jest.spyOn(tagsApiService, 'getTags'); 71 | spy.mockReturnValue(of(mockTags)); 72 | store.tags$.subscribe((tags) => { 73 | expect(tags.length).toBe(2); 74 | expect(tags[1]).toEqual(mockTags[1]); 75 | done(); 76 | }); 77 | store.getTags(); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/services/tags.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ComponentStore, tapResponse } from '@ngrx/component-store'; 3 | import { Tag } from '@devto/core/models'; 4 | import { TagsApiService } from './tags-api.service'; 5 | 6 | interface TagsState { 7 | tags: Tag[]; 8 | } 9 | 10 | @Injectable({ 11 | providedIn: 'root', 12 | }) 13 | export class TagsStore extends ComponentStore { 14 | readonly tags$ = this.select((state) => state.tags); 15 | readonly getTags = this.effect(() => 16 | this.tagsApiS.getTags().pipe( 17 | tapResponse( 18 | (tags) => this.setTags(tags), 19 | (error) => this.logError(error) 20 | ) 21 | ) 22 | ); 23 | 24 | readonly setTags = this.updater((state: TagsState, tags: Tag[]) => ({ 25 | ...state, 26 | tags, 27 | })); 28 | 29 | constructor(private readonly tagsApiS: TagsApiService) { 30 | super({ tags: [] }); 31 | } 32 | 33 | logError(error: unknown) { 34 | console.error(error); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/sidebar-advertisement/__snapshots__/sidebar-advertisement.component.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SidebarAdvertisementComponent should render 1`] = ` 4 | 7 | 8 | 11 | 17 | 18 | 21 | 24 | New Champion-Brand DEV Wear is Here! 25 | 26 | 27 | 28 | 29 | `; 30 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/sidebar-advertisement/sidebar-advertisement.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SidebarAdvertisementComponent } from './sidebar-advertisement.component'; 4 | 5 | describe('SidebarAdvertisementComponent', () => { 6 | let component: SidebarAdvertisementComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [SidebarAdvertisementComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(SidebarAdvertisementComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | 25 | it('should render', () => { 26 | expect(fixture.nativeElement).toMatchSnapshot(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/sidebar-advertisement/sidebar-advertisement.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-sidebar-advertisement', 5 | template: ` 6 | 13 | New Champion-Brand DEV Wear is Here! 14 | 15 | `, 16 | styles: [ 17 | ` 18 | .ad-decription { 19 | text-align: center; 20 | line-height: 1.29em; 21 | } 22 | `, 23 | ], 24 | }) 25 | export class SidebarAdvertisementComponent {} 26 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/sidebar-social-links/sidebar-social-links.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SidebarSocialLinksComponent } from './sidebar-social-links.component'; 4 | 5 | describe('SidebarSocialLinksComponent', () => { 6 | let component: SidebarSocialLinksComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [SidebarSocialLinksComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(SidebarSocialLinksComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | 25 | it('should render', () => { 26 | expect(fixture.nativeElement).toMatchSnapshot(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/sidebar-social-links/sidebar-social-links.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-sidebar-social-links', 5 | templateUrl: './sidebar-social-links.component.html', 6 | styles: [ 7 | ` 8 | :host { 9 | padding: 1rem; 10 | margin-top: 1rem; 11 | display: block; 12 | } 13 | .social-link-icon { 14 | margin: 0 0.5rem; 15 | color: #64707d; 16 | svg, 17 | svg path { 18 | fill: currentColor; 19 | } 20 | } 21 | `, 22 | ], 23 | }) 24 | export class SidebarSocialLinksComponent {} 25 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/sidebar-tags/__snapshots__/sidebar-tags.component.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SidebarTagsComponent should render 1`] = ` 4 | 7 | 10 | 13 | 16 | My Tags 17 | 18 | 23 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | `; 41 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/sidebar-tags/sidebar-tags.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { LetModule } from '@rx-angular/template'; 3 | import { of } from 'rxjs'; 4 | import { TagsStore } from '../services/tags.store'; 5 | 6 | import { SidebarTagsComponent } from './sidebar-tags.component'; 7 | 8 | describe('SidebarTagsComponent', () => { 9 | let component: SidebarTagsComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async () => { 13 | await TestBed.configureTestingModule({ 14 | imports: [LetModule], 15 | declarations: [SidebarTagsComponent], 16 | providers: [{ provide: TagsStore, useValue: mockTagsStore }], 17 | }).compileComponents(); 18 | }); 19 | 20 | beforeEach(() => { 21 | fixture = TestBed.createComponent(SidebarTagsComponent); 22 | component = fixture.componentInstance; 23 | fixture.detectChanges(); 24 | }); 25 | 26 | it('should create', () => { 27 | expect(component).toBeTruthy(); 28 | }); 29 | 30 | it('should render', () => { 31 | expect(fixture.nativeElement).toMatchSnapshot(); 32 | }); 33 | }); 34 | 35 | const mockTagsStore = { 36 | tags: of([ 37 | { 38 | id: 6, 39 | name: 'javascript', 40 | bg_color_hex: '#F7DF1E', 41 | text_color_hex: '#000000', 42 | }, 43 | { 44 | id: 8, 45 | name: 'webdev', 46 | bg_color_hex: '#562765', 47 | text_color_hex: '#ffffff', 48 | }, 49 | ]), 50 | }; 51 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/sidebar-tags/sidebar-tags.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { TagsStore } from '../services/tags.store'; 3 | 4 | @Component({ 5 | selector: 'app-sidebar-tags', 6 | template: ` 7 | 8 | My Tags 9 | 14 | 21 | 24 | 25 | 26 | 27 | 32 | 33 | #{{ tag.name }} 36 | 37 | 38 | loading 39 | `, 40 | styles: [ 41 | ` 42 | .tags-title { 43 | font-size: 16px; 44 | font-weight: 700; 45 | color: #202428; 46 | } 47 | 48 | header { 49 | padding: 0.5rem; 50 | } 51 | 52 | .followed-tags { 53 | height: 42vh; 54 | overflow-y: auto; 55 | a { 56 | display: block; 57 | padding: 0.5rem; 58 | color: #202428; 59 | border-radius: 5px; 60 | width: 100%; 61 | &:hover, 62 | &:active { 63 | background-color: rgba(8, 9, 10, 0.05); 64 | color: #323ebe; 65 | } 66 | } 67 | } 68 | `, 69 | ], 70 | }) 71 | export class SidebarTagsComponent { 72 | tags$ = this.tagsStore.tags$; 73 | constructor(private tagsStore: TagsStore) {} 74 | } 75 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/sidebar/sidebar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 2 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | 5 | import { HomeModule } from '../../home.module'; 6 | import { SidebarAdvertisementComponent } from '../sidebar-advertisement/sidebar-advertisement.component'; 7 | import { SidebarSocialLinksComponent } from '../sidebar-social-links/sidebar-social-links.component'; 8 | import { SidebarTagsComponent } from '../sidebar-tags/sidebar-tags.component'; 9 | import { SidebarComponent } from './sidebar.component'; 10 | 11 | describe('SidebarComponent', () => { 12 | let component: SidebarComponent; 13 | let fixture: ComponentFixture; 14 | 15 | beforeEach(async () => { 16 | await TestBed.configureTestingModule({ 17 | declarations: [ 18 | SidebarComponent, 19 | SidebarAdvertisementComponent, 20 | SidebarTagsComponent, 21 | SidebarSocialLinksComponent, 22 | ], 23 | 24 | imports: [HomeModule, HttpClientTestingModule, RouterTestingModule], 25 | }).compileComponents(); 26 | }); 27 | 28 | beforeEach(() => { 29 | fixture = TestBed.createComponent(SidebarComponent); 30 | component = fixture.componentInstance; 31 | fixture.detectChanges(); 32 | }); 33 | 34 | it('should create', () => { 35 | expect(component).toBeTruthy(); 36 | }); 37 | 38 | it('should render', () => { 39 | expect(fixture.nativeElement).toMatchSnapshot(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /libs/home/src/lib/sidebar/sidebar/sidebar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-sidebar', 5 | templateUrl: './sidebar.component.html', 6 | styles: [ 7 | ` 8 | :host { 9 | display: block; 10 | width: 240px; 11 | } 12 | a { 13 | width: 100%; 14 | padding: 0.5rem; 15 | display: inline-flex; 16 | align-items: center; 17 | color: #202428; 18 | border-radius: 5px; 19 | &:hover, 20 | &:active { 21 | background-color: rgba(8, 9, 10, 0.05); 22 | color: #323ebe; 23 | } 24 | } 25 | 26 | .sidebar-link-icon { 27 | margin-right: 0.5rem; 28 | vertical-align: middle; 29 | width: 1.5rem; 30 | height: 1.5rem; 31 | font-size: 1.25rem; 32 | } 33 | 34 | .more-link { 35 | padding-left: 2.5rem; 36 | font-size: 0.875rem; 37 | color: #64707d; 38 | } 39 | `, 40 | ], 41 | }) 42 | export class SidebarComponent {} 43 | -------------------------------------------------------------------------------- /libs/home/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /libs/home/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true 18 | }, 19 | "angularCompilerOptions": { 20 | "strictInjectionParameters": true, 21 | "strictInputAccessModifiers": true, 22 | "strictTemplates": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /libs/home/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"], 13 | "include": ["**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /libs/home/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/home/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../dist/out-tsc", 6 | "module": "commonjs", 7 | "types": [ 8 | "jest", 9 | "node" 10 | ] 11 | }, 12 | "files": [ 13 | "src/test-setup.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /libs/listings/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": ["devto", "app"], 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": ["devto", "app"], 25 | "style": "kebab-case" 26 | } 27 | ], 28 | "@typescript-eslint/no-non-null-assertion": "off" 29 | } 30 | }, 31 | { 32 | "files": ["*.html"], 33 | "extends": ["plugin:@nrwl/nx/angular-template"], 34 | "rules": {} 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /libs/listings/README.md: -------------------------------------------------------------------------------- 1 | # listings 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test listings` to execute the unit tests. 8 | -------------------------------------------------------------------------------- /libs/listings/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'listings', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | stringifyContentPathRegex: '\\.(html|svg)$', 9 | }, 10 | }, 11 | coverageDirectory: '../../coverage/libs/listings', 12 | transform: { 13 | '^.+\\.(ts|js|html)$': 'jest-preset-angular', 14 | }, 15 | snapshotSerializers: [ 16 | 'jest-preset-angular/build/serializers/no-ng-attributes', 17 | 'jest-preset-angular/build/serializers/ng-snapshot', 18 | 'jest-preset-angular/build/serializers/html-comment', 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /libs/listings/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/libs/listings", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/listings/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@devto/listings", 3 | "version": "0.0.1", 4 | "peerDependencies": { 5 | "@angular/common": "^12.1.0", 6 | "@angular/core": "^12.1.0" 7 | }, 8 | "dependencies": { 9 | "tslib": "^2.2.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /libs/listings/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/listings.module'; 2 | -------------------------------------------------------------------------------- /libs/listings/src/lib/contents/listing-card/listing-card.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ listing.title }} 5 | 6 | 7 | {{ listing.bumped_at | date: 'MMM d' }} 8 | 9 | 10 | 15 | #{{ tag }} 16 | 17 | 18 | 19 | 20 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 45 | 46 | 47 | 48 | {{ listing.author.name }} 49 | 50 | 51 | {{ listing.category }} 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /libs/listings/src/lib/contents/listing-card/listing-card.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ListingCardComponent } from './listing-card.component'; 4 | 5 | describe('ListingCardComponent', () => { 6 | let component: ListingCardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ListingCardComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ListingCardComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/listings/src/lib/contents/listing-card/listing-card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { AuthorListing } from '@devto/core/models'; 3 | 4 | @Component({ 5 | selector: 'app-listing-card', 6 | templateUrl: './listing-card.component.html', 7 | styleUrls: ['./listing-card.component.scss'], 8 | }) 9 | export class ListingCardComponent { 10 | @Input() listing!: AuthorListing; 11 | } 12 | -------------------------------------------------------------------------------- /libs/listings/src/lib/contents/listings-content/listings-content.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ListingsContentComponent } from './listings-content.component'; 4 | 5 | describe('ListingsContentComponent', () => { 6 | let component: ListingsContentComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ListingsContentComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ListingsContentComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/listings/src/lib/contents/listings-content/listings-content.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { map, tap } from 'rxjs/operators'; 4 | import { ListingsStore } from '../service/listings-store'; 5 | 6 | @Component({ 7 | selector: 'app-listings-content', 8 | template: ` 9 | 10 | 15 | 16 | 17 | Load More... 18 | 19 | `, 20 | styles: [ 21 | ` 22 | .listing-container { 23 | font-size: 16px; 24 | } 25 | .listings-cards { 26 | display: grid; 27 | grid-gap: 1rem; 28 | grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); 29 | margin-bottom: 1.5rem; 30 | } 31 | `, 32 | ], 33 | }) 34 | export class ListingsContentComponent implements OnInit { 35 | hasPagination = false; 36 | listingStore$ = this.listingsStore.listings$.pipe( 37 | tap((listing) => (this.hasPagination = listing?.length === 75)) 38 | ); 39 | 40 | constructor( 41 | private listingsStore: ListingsStore, 42 | private activatedRoute: ActivatedRoute 43 | ) {} 44 | 45 | ngOnInit(): void { 46 | this.listingsStore.getListings( 47 | this.activatedRoute.params.pipe(map((param) => param.category)) 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /libs/listings/src/lib/contents/masonary.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { MasonaryDirective } from './masonary.directive'; 2 | 3 | describe('MasonaryDirective', () => { 4 | it('should create an instance', () => { 5 | const directive = new MasonaryDirective(); 6 | expect(directive).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /libs/listings/src/lib/contents/masonary.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, OnDestroy, OnInit } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[appMasonary]', 5 | }) 6 | export class MasonaryDirective implements OnInit, OnDestroy { 7 | private observer = new MutationObserver(() => { 8 | this.cardElments.forEach((cardElement) => { 9 | this.resizeMasonryItem(cardElement as HTMLElement); 10 | }); 11 | }); 12 | 13 | constructor(private gridElement: ElementRef) {} 14 | 15 | get cardElments() { 16 | return Array.from(this.gridElement.nativeElement.children); 17 | } 18 | 19 | ngOnInit() { 20 | this.observer.observe(this.gridElement.nativeElement, { childList: true }); 21 | } 22 | 23 | //Reference:https://w3bits.com/css-grid-masonry/ 24 | resizeMasonryItem = (cardeElment: HTMLElement) => { 25 | /* Get the grid object, its row-gap, and the size of its implicit rows */ 26 | const grid = this.gridElement.nativeElement; 27 | const rowGap = parseInt( 28 | window.getComputedStyle(grid).getPropertyValue('grid-row-gap'), 29 | 10 30 | ); 31 | const rowHeight = 0; 32 | // eslint-disable @typescript-eslint/no-non-null-assertion 33 | const rowSpan = Math.ceil( 34 | (cardeElment.querySelector('.listing-card')!.getBoundingClientRect() 35 | .height + 36 | rowGap) / 37 | (rowHeight + rowGap) 38 | ); 39 | cardeElment.style.gridRowEnd = `span ${rowSpan}`; 40 | }; 41 | 42 | ngOnDestroy() { 43 | this.observer.disconnect(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /libs/listings/src/lib/contents/service/listings-api.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ListingsApiService } from './listings-api.service'; 4 | 5 | describe('ListingsApiService', () => { 6 | let service: ListingsApiService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ListingsApiService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /libs/listings/src/lib/contents/service/listings-api.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpParams } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { ListingCategory, ListingsReponse } from '@devto/core/models'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class ListingsApiService { 10 | constructor(private http: HttpClient) {} 11 | 12 | getListings(category?: ListingCategory): Observable { 13 | const categoryParam = new HttpParams() 14 | .set('category', category || '') 15 | .toString(); 16 | return this.http.get( 17 | `https://dev.to/search/listings?${categoryParam}&listing_search=&page=0&per_page=75&tag_boolean_mode=all` 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /libs/listings/src/lib/contents/service/listings-store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ComponentStore, tapResponse } from '@ngrx/component-store'; 3 | import { Observable } from 'rxjs'; 4 | import { switchMap } from 'rxjs/operators'; 5 | import { ListingCategory, Listings } from '@devto/core/models'; 6 | import { ListingsApiService } from './listings-api.service'; 7 | 8 | interface ListingsState { 9 | listings: Listings; 10 | } 11 | 12 | @Injectable({ 13 | providedIn: 'root', 14 | }) 15 | export class ListingsStore extends ComponentStore { 16 | readonly listings$: Observable = this.select((state) => { 17 | return state.listings; 18 | }); 19 | readonly addListings = this.updater((state, listings: Listings) => ({ 20 | listings: [...state.listings, ...listings], 21 | })); 22 | 23 | readonly loadListings = this.updater((_, listings: Listings) => ({ 24 | listings: [...listings], 25 | })); 26 | 27 | constructor(private listingsApiS: ListingsApiService) { 28 | super({ listings: [] }); 29 | } 30 | 31 | getListings = this.effect((activatedRoute$: Observable) => 32 | activatedRoute$.pipe( 33 | switchMap((categoryParam) => 34 | this.listingsApiS.getListings(categoryParam).pipe( 35 | tapResponse( 36 | (listings) => { 37 | return this.loadListings(listings.result); 38 | }, 39 | (error) => this.logError(error) 40 | ) 41 | ) 42 | ) 43 | ) 44 | ); 45 | 46 | logError(error: unknown) { 47 | console.error(error); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /libs/listings/src/lib/listings-header/listings-header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ListingsHeaderComponent } from './listings-header.component'; 4 | 5 | describe('ListingsHeaderComponent', () => { 6 | let component: ListingsHeaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ListingsHeaderComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ListingsHeaderComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/listings/src/lib/listings-header/listings-header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-listings-header', 5 | template: ` 6 | Listings 7 | 8 | 9 | 10 | Create 11 | 12 | 13 | Manage 14 | 15 | 16 | 17 | `, 18 | styles: [ 19 | ` 20 | li { 21 | list-style: none; 22 | } 23 | .listings-main-title { 24 | font-size: 1.875rem; 25 | margin: 0; 26 | } 27 | .listings-header-actions { 28 | margin: 0; 29 | gap: 0.5rem; 30 | } 31 | `, 32 | ], 33 | }) 34 | export class ListingsHeaderComponent {} 35 | -------------------------------------------------------------------------------- /libs/listings/src/lib/listings.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ListingsComponent } from './listings.component'; 4 | 5 | describe('ListingsComponent', () => { 6 | let component: ListingsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ListingsComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ListingsComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/listings/src/lib/listings.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-listings', 5 | template: ` 6 | 7 | 8 | 9 | 10 | 11 | `, 12 | styles: [ 13 | ` 14 | main { 15 | padding-top: 1rem; 16 | display: grid; 17 | grid-gap: 1rem; 18 | grid-template-columns: 240px 1fr; 19 | } 20 | `, 21 | ], 22 | }) 23 | export class ListingsComponent {} 24 | -------------------------------------------------------------------------------- /libs/listings/src/lib/listings.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ListingsComponent } from './listings.component'; 4 | import { RouterModule } from '@angular/router'; 5 | import { ListingsHeaderComponent } from './listings-header/listings-header.component'; 6 | import { ListingsSidenavComponent } from './sidenav/listings-sidenav/listings-sidenav.component'; 7 | import { ListingsContentComponent } from './contents/listings-content/listings-content.component'; 8 | import { MasonaryDirective } from './contents/masonary.directive'; 9 | import { LetModule, PushModule } from '@rx-angular/template'; 10 | import { ListingCardComponent } from './contents/listing-card/listing-card.component'; 11 | 12 | @NgModule({ 13 | declarations: [ 14 | ListingsComponent, 15 | ListingsHeaderComponent, 16 | ListingsSidenavComponent, 17 | ListingsContentComponent, 18 | MasonaryDirective, 19 | ListingCardComponent, 20 | ], 21 | imports: [ 22 | LetModule, 23 | PushModule, 24 | RouterModule.forChild([ 25 | { 26 | path: '', 27 | component: ListingsComponent, 28 | children: [ 29 | { 30 | path: ':category', 31 | component: ListingsContentComponent, 32 | }, 33 | { 34 | path: '', 35 | component: ListingsContentComponent, 36 | }, 37 | ], 38 | }, 39 | ]), 40 | CommonModule, 41 | ], 42 | }) 43 | export class ListingsModule {} 44 | -------------------------------------------------------------------------------- /libs/listings/src/lib/sidenav/listings-sidenav/listings-sidenav.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ListingsSidenavComponent } from './listings-sidenav.component'; 4 | 5 | describe('ListingsSidenavComponent', () => { 6 | let component: ListingsSidenavComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ListingsSidenavComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ListingsSidenavComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/listings/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /libs/listings/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true 18 | }, 19 | "angularCompilerOptions": { 20 | "strictInjectionParameters": true, 21 | "strictInputAccessModifiers": true, 22 | "strictTemplates": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /libs/listings/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"], 13 | "include": ["**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /libs/listings/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/listings/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../dist/out-tsc", 6 | "module": "commonjs", 7 | "types": [ 8 | "jest", 9 | "node" 10 | ] 11 | }, 12 | "files": [ 13 | "src/test-setup.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /libs/styles/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "devto", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "devto", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nrwl/nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /libs/styles/README.md: -------------------------------------------------------------------------------- 1 | # styles 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test styles` to execute the unit tests. 8 | -------------------------------------------------------------------------------- /libs/styles/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'styles', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | stringifyContentPathRegex: '\\.(html|svg)$', 9 | }, 10 | }, 11 | coverageDirectory: '../../coverage/libs/styles', 12 | transform: { 13 | '^.+\\.(ts|js|html)$': 'jest-preset-angular', 14 | }, 15 | snapshotSerializers: [ 16 | 'jest-preset-angular/build/serializers/no-ng-attributes', 17 | 'jest-preset-angular/build/serializers/ng-snapshot', 18 | 'jest-preset-angular/build/serializers/html-comment', 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /libs/styles/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/libs/styles", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/styles/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@devto/styles", 3 | "version": "0.0.1", 4 | "peerDependencies": { 5 | "@angular/common": "^12.1.1", 6 | "@angular/core": "^12.1.1" 7 | }, 8 | "dependencies": { 9 | "tslib": "^2.2.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /libs/styles/src/index.scss: -------------------------------------------------------------------------------- 1 | @import "./lib/global"; 2 | @import "./lib/article-detail"; 3 | @import "./lib/comments"; 4 | -------------------------------------------------------------------------------- /libs/styles/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/styles.module'; 2 | -------------------------------------------------------------------------------- /libs/styles/src/lib/_comments.scss: -------------------------------------------------------------------------------- 1 | app-comments { 2 | details { 3 | position: relative; 4 | } 5 | .comment-border { 6 | border-radius: 5px; 7 | background: #fff; 8 | color: rgb(8, 9, 10); 9 | box-shadow: rgba(8, 9, 10, 0.1) 0px 0px 0px 1px; 10 | overflow-wrap: anywhere; 11 | padding: 0.25rem; 12 | width: 100%; 13 | } 14 | 15 | summary.closed { 16 | display: block; 17 | align-items: center; 18 | transform: initial; 19 | top: 0; 20 | cursor: pointer; 21 | font-size: 0.875rem; 22 | color: #64707d; 23 | padding: 0.25rem 0.5rem; 24 | font-style: italic; 25 | border-radius: 5px; 26 | background: #f9fafa; 27 | margin-bottom: 1rem; 28 | } 29 | 30 | .comment-header { 31 | padding: 0.5rem 0.75rem 0 0.25rem; 32 | } 33 | 34 | .comment-username { 35 | padding: 0.5rem; 36 | } 37 | 38 | .comments-body { 39 | margin-bottom: 2rem; 40 | } 41 | 42 | .comment-inner-text { 43 | font-size: 1.125rem; 44 | padding: 0 0.75rem; 45 | margin: 0.5rem 0 1rem; 46 | p { 47 | margin-bottom: 1rem; 48 | } 49 | } 50 | 51 | .comment-avatar { 52 | width: 2rem; 53 | margin-right: 0.5rem; 54 | margin-top: 0.75rem; 55 | height: 2rem; 56 | img { 57 | border-radius: 100%; 58 | height: 100%; 59 | display: inline-block; 60 | vertical-align: bottom; 61 | } 62 | } 63 | .app-comments .app-comments .app-comments .app-comments { 64 | padding-left: 0; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /libs/styles/src/lib/styles.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | @NgModule({ 5 | imports: [CommonModule], 6 | }) 7 | export class StylesModule {} 8 | -------------------------------------------------------------------------------- /libs/styles/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /libs/styles/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true 18 | }, 19 | "angularCompilerOptions": { 20 | "strictInjectionParameters": true, 21 | "strictInputAccessModifiers": true, 22 | "strictTemplates": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /libs/styles/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"], 13 | "include": ["**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /libs/styles/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/styles/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../dist/out-tsc", 6 | "module": "commonjs", 7 | "types": [ 8 | "jest", 9 | "node" 10 | ] 11 | }, 12 | "files": [ 13 | "src/test-setup.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /libs/user-profile/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": ["devto","app"], 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": ["devto","app"], 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nrwl/nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /libs/user-profile/README.md: -------------------------------------------------------------------------------- 1 | # user-profile 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test user-profile` to execute the unit tests. 8 | -------------------------------------------------------------------------------- /libs/user-profile/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'user-profile', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | stringifyContentPathRegex: '\\.(html|svg)$', 9 | }, 10 | }, 11 | coverageDirectory: '../../coverage/libs/user-profile', 12 | transform: { 13 | '^.+\\.(ts|js|html)$': 'jest-preset-angular', 14 | }, 15 | snapshotSerializers: [ 16 | 'jest-preset-angular/build/serializers/no-ng-attributes', 17 | 'jest-preset-angular/build/serializers/ng-snapshot', 18 | 'jest-preset-angular/build/serializers/html-comment', 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /libs/user-profile/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/libs/user-profile", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/user-profile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@devto/user-profile", 3 | "version": "0.0.1", 4 | "peerDependencies": { 5 | "@angular/common": "^12.1.1", 6 | "@angular/core": "^12.1.1" 7 | }, 8 | "dependencies": { 9 | "tslib": "^2.2.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /libs/user-profile/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/user-profile.module'; 2 | -------------------------------------------------------------------------------- /libs/user-profile/src/lib/user-header/user-header.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | padding: 1rem; 4 | padding-top: 2rem; 5 | margin: 0 auto; 6 | max-width: 1024px; 7 | } 8 | 9 | header { 10 | border-radius: 5px; 11 | background: #fff; 12 | color: #08090a; 13 | box-shadow: 0 0 0 1px rgba(8, 9, 10, 0.1); 14 | overflow-wrap: anywhere; 15 | text-align: center; 16 | } 17 | 18 | .profile-header { 19 | position: relative; 20 | } 21 | 22 | img { 23 | padding: 0.5rem; 24 | width: 8rem; 25 | height: 8rem; 26 | border-radius: 100%; 27 | } 28 | 29 | .profile-header-details { 30 | padding: 1.5rem; 31 | h1 { 32 | font-weight: 800; 33 | font-size: 1.875rem; 34 | margin-bottom: 0.5rem; 35 | } 36 | 37 | .user-summary { 38 | font-size: 1.125rem; 39 | margin-bottom: 1rem; 40 | color: #202408; 41 | } 42 | } 43 | 44 | .profile-header-meta { 45 | font-size: 0.875rem; 46 | color: #64707d; 47 | margin-bottom: 0.5rem; 48 | display: flex; 49 | flex-wrap: wrap; 50 | align-items: center; 51 | justify-content: center; 52 | gap: 1.5rem; 53 | .item { 54 | display: flex; 55 | align-items: center; 56 | } 57 | svg { 58 | margin-right: 0.5rem; 59 | } 60 | } 61 | 62 | a { 63 | color: #64707d; 64 | } 65 | -------------------------------------------------------------------------------- /libs/user-profile/src/lib/user-header/user-header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UserHeaderComponent } from './user-header.component'; 4 | 5 | describe('UserHeaderComponent', () => { 6 | let component: UserHeaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [UserHeaderComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(UserHeaderComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/user-profile/src/lib/user-header/user-header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { UserDetails } from '@devto/core/models'; 3 | 4 | @Component({ 5 | selector: 'app-user-header', 6 | templateUrl: './user-header.component.html', 7 | styleUrls: ['./user-header.component.scss'], 8 | }) 9 | export class UserHeaderComponent { 10 | @Input() user!: UserDetails; 11 | } 12 | -------------------------------------------------------------------------------- /libs/user-profile/src/lib/user-profile.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UserProfileComponent } from './user-profile.component'; 4 | 5 | describe('UserProfileComponent', () => { 6 | let component: UserProfileComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [UserProfileComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(UserProfileComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/user-profile/src/lib/user-profile.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { UserStore } from '@devto/global-services'; 4 | import { UserArticlesStore } from '@devto/global-services'; 5 | 6 | @Component({ 7 | selector: 'app-user-profile', 8 | template: ` 9 | 10 | 11 | 12 | 13 | 14 | `, 15 | styles: [ 16 | ` 17 | app-article-card { 18 | margin-left: auto; 19 | margin-right: auto; 20 | max-width: 994px; 21 | } 22 | `, 23 | ], 24 | viewProviders: [UserStore, UserArticlesStore], 25 | }) 26 | export class UserProfileComponent implements OnInit { 27 | user$ = this.userStore.user$; 28 | articles$ = this.userArticles.articles$; 29 | constructor( 30 | private userStore: UserStore, 31 | private route: ActivatedRoute, 32 | private userArticles: UserArticlesStore 33 | ) {} 34 | ngOnInit() { 35 | this.userStore.getUser(this.route.snapshot.params.username); 36 | this.userArticles.getArticles({ 37 | username: this.route.snapshot.params.username, 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /libs/user-profile/src/lib/user-profile.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { UserProfileComponent } from './user-profile.component'; 4 | import { UserHeaderComponent } from './user-header/user-header.component'; 5 | import { RouterModule } from '@angular/router'; 6 | import { LetModule, PushModule } from '@rx-angular/template'; 7 | import { ArticleCardModule } from '@devto/global-components'; 8 | 9 | @NgModule({ 10 | declarations: [UserProfileComponent, UserHeaderComponent], 11 | imports: [ 12 | ArticleCardModule, 13 | LetModule, 14 | PushModule, 15 | CommonModule, 16 | RouterModule.forChild([ 17 | { 18 | path: '', 19 | component: UserProfileComponent, 20 | }, 21 | ]), 22 | ], 23 | }) 24 | export class UserProfileModule {} 25 | -------------------------------------------------------------------------------- /libs/user-profile/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /libs/user-profile/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true 18 | }, 19 | "angularCompilerOptions": { 20 | "strictInjectionParameters": true, 21 | "strictInputAccessModifiers": true, 22 | "strictTemplates": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /libs/user-profile/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"], 13 | "include": ["**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /libs/user-profile/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/user-profile/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../dist/out-tsc", 6 | "module": "commonjs", 7 | "types": [ 8 | "jest", 9 | "node" 10 | ] 11 | }, 12 | "files": [ 13 | "src/test-setup.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /libs/videos/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": ["devto","app"], 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": ["devto","app"], 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nrwl/nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /libs/videos/README.md: -------------------------------------------------------------------------------- 1 | # videos 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test videos` to execute the unit tests. 8 | -------------------------------------------------------------------------------- /libs/videos/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'videos', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | stringifyContentPathRegex: '\\.(html|svg)$', 9 | }, 10 | }, 11 | coverageDirectory: '../../coverage/libs/videos', 12 | transform: { 13 | '^.+\\.(ts|js|html)$': 'jest-preset-angular', 14 | }, 15 | snapshotSerializers: [ 16 | 'jest-preset-angular/build/serializers/no-ng-attributes', 17 | 'jest-preset-angular/build/serializers/ng-snapshot', 18 | 'jest-preset-angular/build/serializers/html-comment', 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /libs/videos/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/libs/videos", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/videos/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@devto/videos", 3 | "version": "0.0.1", 4 | "peerDependencies": { 5 | "@angular/common": "^12.1.1", 6 | "@angular/core": "^12.1.1" 7 | }, 8 | "dependencies": { 9 | "tslib": "^2.2.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /libs/videos/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/videos.module'; 2 | -------------------------------------------------------------------------------- /libs/videos/src/lib/videos-list/services/videos-list.store.spec.ts: -------------------------------------------------------------------------------- 1 | import { VideosListStore } from './videos-list.store'; 2 | 3 | describe('VideosListStore', () => { 4 | it('should create an instance', () => { 5 | expect(new VideosListStore()).toBeTruthy(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /libs/videos/src/lib/videos-list/services/videos-list.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ComponentStore, tapResponse } from '@ngrx/component-store'; 3 | import { Observable } from 'rxjs'; 4 | import { switchMap } from 'rxjs/operators'; 5 | import { VideosList } from '@devto/core/models'; 6 | import { VideoslistApiService } from './videoslist-api.service'; 7 | 8 | interface VideosListState { 9 | VideosList: VideosList[]; 10 | } 11 | 12 | @Injectable({ 13 | providedIn: 'root', 14 | }) 15 | export class VideosListStore extends ComponentStore { 16 | readonly VideosList$ = this.select((state) => state.VideosList); 17 | readonly setVideoslist = this.updater( 18 | (state: VideosListState, VideosList: VideosList[]) => ({ 19 | ...state, 20 | VideosList: [...state.VideosList, ...VideosList], 21 | }) 22 | ); 23 | 24 | readonly getVideoslist = this.effect( 25 | (params: Observable>) => 26 | params.pipe( 27 | switchMap((params) => 28 | this.VideoslistApiS.getVideoslist(params).pipe( 29 | tapResponse( 30 | (VideosList) => this.setVideoslist(VideosList), 31 | (error) => this.logError(error) 32 | ) 33 | ) 34 | ) 35 | ) 36 | ); 37 | constructor(private VideoslistApiS: VideoslistApiService) { 38 | super({ VideosList: [] }); 39 | } 40 | 41 | logError(error: unknown) { 42 | console.error(error); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /libs/videos/src/lib/videos-list/services/videoslist-api.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { VideoslistApiService } from './videoslist-api.service'; 4 | 5 | describe('VideoslistApiService', () => { 6 | let service: VideoslistApiService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(VideoslistApiService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /libs/videos/src/lib/videos-list/services/videoslist-api.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { VideosList } from '@devto/core/models'; 5 | import { environment } from '@devto/environments'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class VideoslistApiService { 11 | constructor(private http: HttpClient) {} 12 | 13 | getVideoslist(params?: Record): Observable { 14 | const newParams = new HttpParams({ fromObject: params }).toString(); 15 | return this.http.get( 16 | `${environment.baseApi}/videos?${newParams}` 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /libs/videos/src/lib/videos-list/video-card/video-card.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { VideoCardComponent } from './video-card.component'; 4 | 5 | describe('VideoCardComponent', () => { 6 | let component: VideoCardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [VideoCardComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(VideoCardComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/videos/src/lib/videos-list/video-card/video-card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { VideosList } from '@devto/core/models'; 3 | @Component({ 4 | selector: 'app-video-card', 5 | template: ` 6 | 7 | 13 | {{ 14 | video.video_duration_in_minutes 15 | }} 16 | 17 | 18 | {{ video.title }} 19 | 20 | {{ video.user.name }} 21 | 22 | `, 23 | styles: [ 24 | ` 25 | .video-collection { 26 | display: inline; 27 | 28 | .single-video-article { 29 | border: solid 1px #dbdbdb; 30 | margin: 5px; 31 | width: 100%; 32 | padding-bottom: 8px; 33 | border-radius: 3px; 34 | display: inline-block; 35 | .video-image { 36 | position: relative; 37 | padding-top: 56%; 38 | border-top-left-radius: 3px; 39 | border-top-right-radius: 3px; 40 | background: #0a0a0a no-repeat center center; 41 | -webkit-background-size: cover; 42 | -moz-background-size: cover; 43 | -o-background-size: cover; 44 | background-size: cover; 45 | } 46 | .video-timestamp { 47 | position: absolute; 48 | font-size: 0.7em; 49 | bottom: 11px; 50 | right: 4px; 51 | background-color: rgba(0, 0, 0, 0.8); 52 | color: #ffffff; 53 | padding: 2px 5px 3px; 54 | font-weight: 500; 55 | border-radius: 3px; 56 | } 57 | 58 | p { 59 | margin: 0px; 60 | padding: 2px 8px; 61 | max-height: 100%; 62 | max-width: 90%; 63 | overflow: hidden; 64 | white-space: nowrap; 65 | text-overflow: ellipsis; 66 | color: #08090a; 67 | font-size: 0.95em; 68 | } 69 | 70 | p.video-username { 71 | color: #64707d; 72 | font-size: 0.88em; 73 | } 74 | } 75 | } 76 | 77 | @media screen and (min-width: 550px) { 78 | .video-collection .single-video-article { 79 | width: 47%; 80 | } 81 | } 82 | @media screen and (min-width: 739px) { 83 | .video-collection .single-video-article { 84 | width: 31%; 85 | } 86 | } 87 | `, 88 | ], 89 | }) 90 | export class VideoCardComponent { 91 | @Input() video!: VideosList; 92 | } 93 | -------------------------------------------------------------------------------- /libs/videos/src/lib/videos-list/videos-header/videos-header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { VideosHeaderComponent } from './videos-header.component'; 4 | 5 | describe('VideosHeaderComponent', () => { 6 | let component: VideosHeaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [VideosHeaderComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(VideosHeaderComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/videos/src/lib/videos-list/videos-header/videos-header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-videos-header', 5 | template: ` 6 | DEV Community on Video 7 | `, 8 | styles: [ 9 | ` 10 | .video-page-title { 11 | font-size: calc(0.9vw + 10px); 12 | text-align: center; 13 | } 14 | `, 15 | ], 16 | }) 17 | export class VideosHeaderComponent {} 18 | -------------------------------------------------------------------------------- /libs/videos/src/lib/videos.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { VideosComponent } from './videos.component'; 4 | 5 | describe('VideosComponent', () => { 6 | let component: VideosComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [VideosComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(VideosComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/videos/src/lib/videos.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { VideosListStore } from './videos-list/services/videos-list.store'; 3 | 4 | @Component({ 5 | selector: 'app-videos', 6 | template: ` 7 | 8 | 9 | 10 | 11 | `, 12 | }) 13 | export class VideosComponent implements OnInit { 14 | page = '0'; 15 | videosList$ = this.VideosListStore.VideosList$; 16 | 17 | constructor(private VideosListStore: VideosListStore) {} 18 | 19 | ngOnInit(): void { 20 | this.VideosListStore.getVideoslist({ 21 | page: this.page, 22 | signature: '4072170', 23 | }); 24 | console.log(this.videosList$); 25 | } 26 | 27 | onScrollingFinished() { 28 | this.page = (Number(this.page) + 1).toString(); 29 | this.VideosListStore.getVideoslist({ 30 | page: this.page, 31 | signature: '4072170', 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /libs/videos/src/lib/videos.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterModule } from '@angular/router'; 4 | import { LetModule, PushModule } from '@rx-angular/template'; 5 | import { VideosComponent } from './videos.component'; 6 | import { VideosHeaderComponent } from './videos-list/videos-header/videos-header.component'; 7 | import { VideoCardComponent } from './videos-list/video-card/video-card.component'; 8 | import { ScrollTrackerDirective } from '@devto/global-components'; 9 | @NgModule({ 10 | declarations: [ 11 | VideosComponent, 12 | VideosHeaderComponent, 13 | VideoCardComponent, 14 | ScrollTrackerDirective, 15 | ], 16 | imports: [ 17 | LetModule, 18 | PushModule, 19 | CommonModule, 20 | RouterModule.forChild([ 21 | { 22 | path: '', 23 | component: VideosComponent, 24 | }, 25 | ]), 26 | ], 27 | }) 28 | export class VideosModule {} 29 | -------------------------------------------------------------------------------- /libs/videos/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /libs/videos/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true 18 | }, 19 | "angularCompilerOptions": { 20 | "strictInjectionParameters": true, 21 | "strictInputAccessModifiers": true, 22 | "strictTemplates": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /libs/videos/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"], 13 | "include": ["**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /libs/videos/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/videos/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../dist/out-tsc", 6 | "module": "commonjs", 7 | "types": [ 8 | "jest", 9 | "node" 10 | ] 11 | }, 12 | "files": [ 13 | "src/test-setup.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "devto", 3 | "affected": { 4 | "defaultBase": "main" 5 | }, 6 | "implicitDependencies": { 7 | "angular.json": "*", 8 | "package.json": "*", 9 | "tslint.json": "*", 10 | ".eslintrc.json": "*", 11 | "nx.json": "*", 12 | "tsconfig.base.json": "*" 13 | }, 14 | "projects": { 15 | "article-detail": { 16 | "tags": [] 17 | }, 18 | "core-app-routes": { 19 | "tags": [] 20 | }, 21 | "core-models": { 22 | "tags": [] 23 | }, 24 | "devto": { 25 | "tags": [], 26 | "implicitDependencies": ["styles"] 27 | }, 28 | "devto-e2e": { 29 | "tags": [], 30 | "implicitDependencies": ["devto"] 31 | }, 32 | "environments": { 33 | "tags": [] 34 | }, 35 | "global-components": { 36 | "tags": [] 37 | }, 38 | "global-constants": { 39 | "tags": [] 40 | }, 41 | "global-services": { 42 | "tags": [] 43 | }, 44 | "home": { 45 | "tags": [] 46 | }, 47 | "listings": { 48 | "tags": [] 49 | }, 50 | "styles": { 51 | "tags": [] 52 | }, 53 | "user-profile": { 54 | "tags": [] 55 | }, 56 | "videos": { 57 | "tags": [] 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tools/schematics/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajitsinghkaler/devto-clone/f8fa3fd2d220bc25bc6c19d420c15f3135b685d7/tools/schematics/.gitkeep -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"], 9 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "outDir": "./dist/out-tsc", 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "noImplicitReturns": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "sourceMap": true, 11 | "declaration": false, 12 | "downlevelIteration": true, 13 | "experimentalDecorators": true, 14 | "moduleResolution": "node", 15 | "importHelpers": true, 16 | "target": "es2017", 17 | "module": "es2020", 18 | "lib": ["es2018", "dom"], 19 | "paths": { 20 | "@devto/article-detail": ["libs/article-detail/src/index.ts"], 21 | "@devto/core/app-routes": ["libs/core/app-routes/src/index.ts"], 22 | "@devto/core/models": ["libs/core/models/src/index.ts"], 23 | "@devto/environments": ["libs/environments/src/index.ts"], 24 | "@devto/global-components": ["libs/global-components/src/index.ts"], 25 | "@devto/global-constants": ["libs/global-constants/src/index.ts"], 26 | "@devto/global-services": ["libs/global-services/src/index.ts"], 27 | "@devto/home": ["libs/home/src/index.ts"], 28 | "@devto/listings": ["libs/listings/src/index.ts"], 29 | "@devto/styles": ["libs/styles/src/index.ts"], 30 | "@devto/user-profile": ["libs/user-profile/src/index.ts"], 31 | "@devto/videos": ["libs/videos/src/index.ts"] 32 | }, 33 | "rootDir": "." 34 | }, 35 | "angularCompilerOptions": { 36 | "enableI18nLegacyMessageIdFormat": false, 37 | "strictInjectionParameters": true, 38 | "strictInputAccessModifiers": true, 39 | "strictTemplates": true 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "CommonJS" 5 | } 6 | } 7 | --------------------------------------------------------------------------------