5 of 6
deleted 78 characters in body; edited tags
Peilonrayz
  • 42.4k
  • 7
  • 72
  • 152

Web developer profile page in Angular 2+

I recently took a class on Angular 2+ at Code School.

I then rebuilt my homepage using Angular 4.

I would greatly appreciate if any developer experienced with Angular 2+ can check out my code on git and offer constructive feedback.

This site features loading content from JSON services into dynamic templates, unit tests, and advanced CSS.

Full code here: https://github.com/garyv/garyv

Projects page here: http://garyvonschilling.com/work

<!-- projects.component.html -->
<div class='big-margin-bottom'>
  <h3>Technology used:</h3>
  <ul class='tags row'>
    <li *ngFor="let tag of projectTags.tags" 
        class="col tag" [class.active]="projectTags.isActive(tag)" 
        (click)="toggleTag(tag)">
      {{tag}} 
      <i [class.fa]="true" 
         [class.fa-check]="projectTags.isActive(tag)"
         [class.fa-minus-circle]="!projectTags.isActive(tag)"></i>
    </li>
  </ul>
</div>

<div class='row' [class.fade-in]="!skipFade">
  <div *ngIf="!projectTags.activeProjects.length">
    <h4>
      Click a tag above to see projects
      <i class='fa fa-level-up'></i>
    </h4>
  </div>
  <div *ngFor="let project of projectTags.activeProjects"
       class='col project grid-4 half big-margin-bottom medium-padding' 
       [class.active]="project.active" [class.hidden]="!project.active">
    <h4>
      <a [routerLink]="[project.friendlyId]">{{project.title}}</a>
    </h4>
    <a *ngIf="project.image?.src" [routerLink]="[project.friendlyId]">
      <img alt='' 
       [src]="project.image.src" 
       [srcset]="project.image.srcset" 
       sizes="(min-width: 37.5em) 30vw, 48vw" />
    </a>
    <div class='tags'>
      Tags: 
      <span [innerHTML]="projectTags.activeProjectTags(project)"></span>
    </div>
  </div>

</div>
// projects.component.ts
import { Component, Input, OnInit } from '@angular/core';

import { Project } from './project/project.model';
import { ProjectTags } from './project-tags.model';
import { ProjectsService } from './projects.service';
import { StateService } from '../state/state.service';

@Component({
  selector: 'app-projects',
  templateUrl: './projects.component.html',
  styleUrls: ['./projects.component.css']
})
export class ProjectsComponent implements OnInit {

  projects: Project[];
  projectTags: ProjectTags;
  @Input() skipFade: boolean;
  
  constructor(private projectsService: ProjectsService) { }

  ngOnInit() {
    this.projectTags = new ProjectTags();

    this.projectsService.getProjects()
      .subscribe( (projects) => {
        this.projects = projects;
        this.projectTags.populateTags(projects);
      });
  }

  toggleTag(tag) {
    this.projectTags.toggleTag(tag, this.projects);
  }
}
// projects.component.spec.ts
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By }              from '@angular/platform-browser';
import { DebugElement }    from '@angular/core';
import { HttpModule } from '@angular/http';
import { Observable } from 'rxjs/Observable';

import 'rxjs/add/observable/of';

import { ProjectsComponent } from './projects.component';

import { Project } from './project/project.model';
import { ProjectComponent } from './project/project.component';
import { ProjectTags } from './project-tags.model';
import { ProjectsService } from './projects.service';
import { StateService } from '../state/state.service';

import { Router, RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';

describe('ProjectsComponent', () => {
  let component: ProjectsComponent;
  let fixture: ComponentFixture<ProjectsComponent>;
  let debugElement: DebugElement;

  let projectsService: ProjectsService;

  let mockProjects: Project[] = [
    { 
      friendlyId: 'example-title',
      image: {
        src: 'https://www.fillmurray.com/400/300/',
      },
      link: {
        address: '//example.com',
        text: 'example text'
      },
      text: '<p>This is an example project.</p>',
      title: 'Example Title',
      tags: ['Example Tag', 'Same']
    }, 
    { 
      friendlyId: 'lorem-ipsum-title',
      image: {
        src: 'https://www.fillmurray.com/400/300/g',
      },
      link: {
        address: '//lorempixel.com',
        text: 'lorem ipsum'
      },
      text: '<p>Lorem ipsum dolor sit amet.</p>',
      title: 'Lorem Ipsum Title',
      tags: ['Lorem', 'Same']
    }
  ];

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterModule, 
        HttpModule,
        RouterTestingModule.withRoutes(
          [{path: 'work/:friendly-id', component: ProjectComponent}]
        )
      ],
      declarations: [ 
        ProjectsComponent,
        ProjectComponent
      ],  
      providers: [
        ProjectsService,
        StateService
      ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(ProjectsComponent);
    component = fixture.componentInstance;
    debugElement = fixture.debugElement;

    projectsService = fixture.debugElement.injector.get(ProjectsService);

    spyOn(projectsService, 'getProjects')
      .and.returnValue(Observable.of(mockProjects));

    StateService.set('tags', '["Same"]');
    fixture.detectChanges();
  });

  it('should be created', () => {
    expect(component).toBeTruthy();
  });

  it('should list tags', () => {
    let tagsElement = debugElement.query(By.css('.tags li'));
    expect(tagsElement.nativeElement.textContent).toContain('Example Tag'); 
  });

  it('should list projects', () => {
    let projectElements = debugElement.query(By.css('.project'));
    expect(projectElements.nativeElement.textContent).toContain('Example Title'); 
  });

  it('should link to individual project page', () => {
    let projectLink = debugElement.query(By.css('a[href$=example-title]')); 
    expect(projectLink).toBeTruthy();   
  });

  it('should hide inactive projects', () => {
    component.projectTags.activeProjects.splice(0, 1);
    fixture.detectChanges();
    let projectElements = debugElement.query(By.css('.project'));
    expect(projectElements.nativeElement.textContent).toContain('Lorem Ipsum'); 
  });
});
// projects.service.ts
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';

import { Project } from './project/project.model';

@Injectable()
export class ProjectsService {

  constructor(private http:Http) {}

  getProjects() {
    return this.http.get('app/projects/projects.json')
      .map( (response) => {
        let projects = <Project[]>response.json().projects;
        for (let project of projects) {
          if (project.title && !project.friendlyId) {
            project.friendlyId = ProjectsService.getFriendlyId(project.title);
          }                   
        }
      return projects;
     });
  }

  static getFriendlyId(title: string): string {
    return title.toLowerCase()
      .replace(/\W+/g, '-')
      .replace(/^-|-$/g, '');
  }
}
// projects.service.spec.ts
import { TestBed, async, inject } from '@angular/core/testing';
import { 
  HttpModule, 
  Http, 
  Response,
  ResponseOptions, 
  XHRBackend
} from '@angular/http';
import { MockBackend } from '@angular/http/testing';

import { ProjectsService } from './projects.service';

import { Project } from './project/project.model';

describe('ProjectsService', () => {
  let projectsService: ProjectsService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpModule],
      providers: [
        ProjectsService,
        { provide: XHRBackend, useClass: MockBackend }
      ]
    });
  });

  describe('getProjects()', () => {

    it('should return an Observable<Project[]>', 

      inject([ProjectsService, XHRBackend], (projectsService, mockBackend) => {
        
        const mockResponse = {
          projects: [
            { 
              //friendlyId: 'example-title',
              image: {
                src: 'https://www.fillmurray.com/400/300/',
              },
              link: {
                address: '//example.com',
                text: 'example text'
              },
              text: '<p>This is an example project.</p>',
              title: 'Example Title',
              tags: ['Example Tag', 'Same']
            }, 
            { 
              //friendlyId: 'lorem-ipsum',
              image: {
                src: 'http://lorempixel.com/400/300/',
              },
              link: {
                address: '//lipsum.com',
                text: 'lorem ipsum'
              },
              text: '<p>Lorem ipsum dolor sit amet.</p>',
              title: 'Lorem Ipsum',
              tags: ['Lorem', 'Same']
            }
          ]
        };

        mockBackend.connections.subscribe( (connection) => {
          let response = new ResponseOptions({body: JSON.stringify(mockResponse)});
          connection.mockRespond(new Response(response));
        });

        projectsService.getProjects().subscribe( (projects) => {
          expect(projects.length).toEqual(2);     
          expect(projects[0].title).toEqual('Example Title');
          expect(projects[1].title).toEqual('Lorem Ipsum');
          expect(projects[1].friendlyId).toEqual('lorem-ipsum');
          expect(projects[1].image).toEqual({src: 'http://lorempixel.com/400/300/'});
          expect(projects[1].link).toEqual({address: '//lipsum.com', text: 'lorem ipsum'});
          expect(projects[1].text).toEqual('<p>Lorem ipsum dolor sit amet.</p>');
          expect(projects[1].tags).toEqual(['Lorem', 'Same']);
        });
      })
    );

    describe('getFriendlyId()', () => {
      it('should make titles url friendly', () => {
        let friendlyId = ProjectsService.getFriendlyId(" ¿ HellO  World #👋🌎 !! ");
        expect(friendlyId).toEqual('hello-world');
      });
    });

  });
});
// projects-tags.model.ts
import { Input, OnInit } from '@angular/core';

import { Project } from './project/project.model';
import { StateService } from '../state/state.service';

const defaultTags:string[] = ['JavaScript', 'Ruby on Rails'];
const storageKey:string = 'tags';

export class ProjectTags {
  tags: string[] = [];
  activeTags: string[] = [];
  activeProjects: Project[] = [];
  @Input() highlightTag: string;

  constructor() { 
    this.activeTags = JSON.parse(StateService.get(storageKey));
    if (!this.activeTags || !this.activeTags.length) {
      this.activeTags = defaultTags; 
    }
  }

  isActive(tag:string):boolean {
    return this.activeTags.indexOf(tag) != -1;
  }

  populateTags(projects:Project[]):void {
    this.activeProjects = [];
    for (let project of projects) {
      project.active = false;
      for (let tag of project.tags) {
        if (this.tags.indexOf(tag) == -1) {
          this.tags.push(tag);
        }
        if (!project.active && this.activeTags.indexOf(tag) != -1) {
          project.active = true;
        }
      }
      if (project.active) {
        this.activeProjects.push(project);
      }
    }
    this.tags = this.tags.sort();
    StateService.set(storageKey, JSON.stringify(this.activeTags));
  }

  toggleTag(tag:string, projects:Project[]):void {
    let index = this.activeTags.indexOf(tag);

    if (index == -1) {
      this.highlightTag = tag;
      this.activeTags.push(tag);
    } else {
      this.highlightTag = null;
      this.activeTags.splice(index, 1);
    }

    this.populateTags(projects);
  }

  activeProjectTags(project:Project):string {
    let tags:string[] = [];
    for (let tag of project.tags) {
      if (this.isActive(tag)) {
        if (tag == this.highlightTag) {
          tags.push(`<span class='highlight'>${tag}</span>`);
        } else {
          tags.push(`<span>${tag}</span>`);
        }
      }
    }
    return tags.join(', ');
  }
}