From 12e8aadf7b9fff49cc4754e82a02a78dd24ad24e Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Nakazone Batista Date: Thu, 28 Dec 2023 21:28:36 -0300 Subject: [PATCH] Initial Implementation of Project Cards --- src/app/app-router.module.ts | 5 ++ src/app/app.module.ts | 2 + src/app/header/header.component.ts | 8 +- .../project-card/project-card.component.css | 74 ++++++++++++++++++ .../project-card/project-card.component.html | 41 ++++++++++ .../project-card.component.spec.ts | 21 +++++ .../project-card/project-card.component.ts | 33 ++++++++ src/app/projects/projects.component.css | 8 ++ src/app/projects/projects.component.html | 6 ++ src/app/projects/projects.component.spec.ts | 21 +++++ src/app/projects/projects.component.ts | 27 +++++++ src/app/projects/projects.module.ts | 21 +++++ src/app/shared/auth/auth.service.ts | 4 - .../github-service/github.service.spec.ts | 16 ++++ .../shared/github-service/github.service.ts | 78 +++++++++++++++++++ src/app/shared/model/project/project.model.ts | 17 ++++ src/environments/environment.prod.ts | 1 + src/environments/environment.ts | 1 + 18 files changed, 376 insertions(+), 8 deletions(-) create mode 100644 src/app/projects/project-card/project-card.component.css create mode 100644 src/app/projects/project-card/project-card.component.html create mode 100644 src/app/projects/project-card/project-card.component.spec.ts create mode 100644 src/app/projects/project-card/project-card.component.ts create mode 100644 src/app/projects/projects.component.css create mode 100644 src/app/projects/projects.component.html create mode 100644 src/app/projects/projects.component.spec.ts create mode 100644 src/app/projects/projects.component.ts create mode 100644 src/app/projects/projects.module.ts create mode 100644 src/app/shared/github-service/github.service.spec.ts create mode 100644 src/app/shared/github-service/github.service.ts create mode 100644 src/app/shared/model/project/project.model.ts diff --git a/src/app/app-router.module.ts b/src/app/app-router.module.ts index 43d8625..c05a30d 100644 --- a/src/app/app-router.module.ts +++ b/src/app/app-router.module.ts @@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common'; import { RouterModule, Routes } from '@angular/router'; import { HomeComponent } from './home/home.component'; import { CallbackComponent } from './header/header-popup/callback/callback.component'; +import {ProjectsComponent} from "./projects/projects.component"; const routes: Routes = [ { @@ -14,6 +15,10 @@ const routes: Routes = [ path: 'home', component: HomeComponent, }, + { + path: 'projects', + component: ProjectsComponent, + }, { path: 'callback', component: CallbackComponent, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 9d85a39..98c88c1 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -11,6 +11,7 @@ import { ServiceWorkerModule } from '@angular/service-worker'; import { environment } from '../environments/environment'; import { FooterComponent } from './footer/footer.component'; import {HomeModule} from "./home/home.module"; +import {ProjectsModule} from "./projects/projects.module"; @NgModule({ declarations: [AppComponent, FooterComponent], @@ -18,6 +19,7 @@ import {HomeModule} from "./home/home.module"; BrowserModule, HeaderModule, HomeModule, + ProjectsModule, AppRouterModule, AppServiceWorkerModule, SharedModule, diff --git a/src/app/header/header.component.ts b/src/app/header/header.component.ts index 4f75f3f..c13684f 100644 --- a/src/app/header/header.component.ts +++ b/src/app/header/header.component.ts @@ -25,7 +25,7 @@ import { MyProfileComponent } from './header-popup/my-profile/my-profile.compone export class HeaderComponent implements OnInit, OnDestroy { pages: { name: string; route: string }[] = [ { name: 'Home', route: '/home' }, - { name: 'Projects', route: '/home' }, + { name: 'Projects', route: '/projects' }, { name: 'Contact', route: '/home' }, ]; @@ -38,13 +38,13 @@ export class HeaderComponent implements OnInit, OnDestroy { userSliderStatus: boolean = false; @ViewChild('profileBtn') - profileBtnElementRef!: ElementRef; + profileBtnElementRef!: ElementRef; @ViewChild('profileDropdown') - profileDropdownElementRef!: ElementRef; + profileDropdownElementRef!: ElementRef; @ViewChild('user') - userElementRef!: ElementRef; + userElementRef!: ElementRef; loggedUser!: User | null; diff --git a/src/app/projects/project-card/project-card.component.css b/src/app/projects/project-card/project-card.component.css new file mode 100644 index 0000000..12d44ca --- /dev/null +++ b/src/app/projects/project-card/project-card.component.css @@ -0,0 +1,74 @@ +.card .inverse-card-image { + flex-direction: row-reverse; +} + +.card .card-content { + height: 350px; + width: 100%; + padding: 40px; + display: flex; + flex-direction: column; + justify-content: space-around; +} +.card .card-content > * { + margin: 10px; +} + +.card .card-content .card-content-h { + width: 80%; +} + +.card .card-content .card-title { + font-size: 2rem; + font-weight: 500; + justify-content: left; +} +.card .card-content .card-text { + font-size: 1.2rem; + font-weight: 300; +} + + +.card .card-content .card-content-f { + display: flex; + justify-content: center; + flex-direction: row; +} +.card .card-content .card-content-f .card-info { + display: flex; + flex-direction: column; +} +.card .card-content .card-content-f .card-stats { + display: flex; + flex-direction: column; + justify-content: space-around; +} + + +.stat-item { + margin: 5px 0px 5px 0px; + display: flex; + flex-direction: row; + align-items: start; + align-content: start; +} +.stat-item .stat-icon { + margin: 0px 15px 0px 0px; + width: 20px; + height: 20px; + font-size: 1rem; + + display: flex; +} +.stat-item span { + font-size: 1rem; + font-weight: 300; + margin: 0; + justify-content: left; +} + +.stats-inline { + margin: 0; + width: 100%; + flex-direction: row !important; +} diff --git a/src/app/projects/project-card/project-card.component.html b/src/app/projects/project-card/project-card.component.html new file mode 100644 index 0000000..02af6f7 --- /dev/null +++ b/src/app/projects/project-card/project-card.component.html @@ -0,0 +1,41 @@ +
+
+
+

+ {{project.name}} +

+

{{project.description}}

+
+
+
+

Language: {{project.languages}}

+
+
+
+
+ +
+ {{project.license}} +
+
+
+ +
+ {{project.stars}} +
+
+
+ +
+ {{project.forks}} +
+
+
+ +
+ {{project.watchers}} +
+
+
+
+
diff --git a/src/app/projects/project-card/project-card.component.spec.ts b/src/app/projects/project-card/project-card.component.spec.ts new file mode 100644 index 0000000..74de6b1 --- /dev/null +++ b/src/app/projects/project-card/project-card.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProjectCardComponent } from './project-card.component'; + +describe('ProjectCardComponent', () => { + let component: ProjectCardComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ProjectCardComponent] + }); + fixture = TestBed.createComponent(ProjectCardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/projects/project-card/project-card.component.ts b/src/app/projects/project-card/project-card.component.ts new file mode 100644 index 0000000..eb7f79d --- /dev/null +++ b/src/app/projects/project-card/project-card.component.ts @@ -0,0 +1,33 @@ +import {Component, Input} from '@angular/core'; +import { faCodeFork, faEye, faStar } from '@fortawesome/free-solid-svg-icons'; +import {Project} from "../../shared/model/project/project.model"; +import {faScaleBalanced} from "@fortawesome/free-solid-svg-icons/faScaleBalanced"; + +@Component({ + selector: 'app-project-card', + templateUrl: './project-card.component.html', + styleUrls: ['./project-card.component.css'] +}) +export class ProjectCardComponent { + @Input() inverted: boolean = false; + + @Input() project!: Project; + + // Stats Icons Definitions + faLicense = faScaleBalanced; + + faStars = faStar; + + faCodeFork = faCodeFork; + + faEye = faEye; + + get hasLicense(): boolean { + return this.project.license !== undefined; + } + + get hasLanguage(): boolean { + return this.project.languages !== undefined && + this.project.languages?.length > 0; + } +} diff --git a/src/app/projects/projects.component.css b/src/app/projects/projects.component.css new file mode 100644 index 0000000..fc35c46 --- /dev/null +++ b/src/app/projects/projects.component.css @@ -0,0 +1,8 @@ +app-project-card { + display: flex; + flex-direction: column; + margin-top: 25px; +} +app-project-card:last-child { + margin-bottom: 25px; +} diff --git a/src/app/projects/projects.component.html b/src/app/projects/projects.component.html new file mode 100644 index 0000000..6604b95 --- /dev/null +++ b/src/app/projects/projects.component.html @@ -0,0 +1,6 @@ +
+
+ + +
+
diff --git a/src/app/projects/projects.component.spec.ts b/src/app/projects/projects.component.spec.ts new file mode 100644 index 0000000..d396b2d --- /dev/null +++ b/src/app/projects/projects.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProjectsComponent } from './projects.component'; + +describe('ProjectsComponent', () => { + let component: ProjectsComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ProjectsComponent] + }); + fixture = TestBed.createComponent(ProjectsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/projects/projects.component.ts b/src/app/projects/projects.component.ts new file mode 100644 index 0000000..23da837 --- /dev/null +++ b/src/app/projects/projects.component.ts @@ -0,0 +1,27 @@ +import {Component, OnInit} from '@angular/core'; +import {GithubService} from "../shared/github-service/github.service"; +import {Project} from "../shared/model/project/project.model"; + +@Component({ + selector: 'app-projects', + templateUrl: './projects.component.html', + styleUrls: ['./projects.component.css'] +}) +export class ProjectsComponent implements OnInit { + projects!: Project[]; + + constructor(private githubService: GithubService) { + } + + ngOnInit(): void { + this.projects = []; + this.githubService.getProjects().subscribe((project: Project) => { + this.projects.push(project); + }); + } + + identifyProject(index: number, project: Project) { + return project.name; + } + +} diff --git a/src/app/projects/projects.module.ts b/src/app/projects/projects.module.ts new file mode 100644 index 0000000..665f97d --- /dev/null +++ b/src/app/projects/projects.module.ts @@ -0,0 +1,21 @@ +import {NgModule} from "@angular/core"; +import {ProjectsComponent} from "./projects.component"; +import {CommonModule, NgOptimizedImage} from "@angular/common"; +import { ProjectCardComponent } from './project-card/project-card.component'; +import {MatIconModule} from "@angular/material/icon"; +import {FontAwesomeModule} from "@fortawesome/angular-fontawesome"; + +@NgModule({ + declarations: [ + ProjectsComponent, + ProjectCardComponent + ], + imports: [ + CommonModule, + NgOptimizedImage, + MatIconModule, + FontAwesomeModule + ], + exports: [] +}) +export class ProjectsModule { } diff --git a/src/app/shared/auth/auth.service.ts b/src/app/shared/auth/auth.service.ts index b3aedae..678376b 100644 --- a/src/app/shared/auth/auth.service.ts +++ b/src/app/shared/auth/auth.service.ts @@ -2,19 +2,15 @@ import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { first, - firstValueFrom, map, Observable, of, Subject, - take, - tap, } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { environment } from 'src/environments/environment'; import { HttpError } from '../model/httpError/httpError.model'; import { User } from '../model/user/user.model'; -import * as http from 'http'; @Injectable({ providedIn: 'root', diff --git a/src/app/shared/github-service/github.service.spec.ts b/src/app/shared/github-service/github.service.spec.ts new file mode 100644 index 0000000..bcef5d4 --- /dev/null +++ b/src/app/shared/github-service/github.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { GithubServiceService } from './github.service'; + +describe('GithubServiceService', () => { + let service: GithubServiceService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(GithubServiceService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/shared/github-service/github.service.ts b/src/app/shared/github-service/github.service.ts new file mode 100644 index 0000000..5905cea --- /dev/null +++ b/src/app/shared/github-service/github.service.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@angular/core'; +import {Language, Project} from "../model/project/project.model"; +import {HttpClient} from "@angular/common/http"; +import { + map, mergeMap, + Observable, + pipe, switchMap, take +} from 'rxjs'; +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class GithubService { + + GITHUB_API_URL = 'https://api.github.com'; + + GITHUB_USER = environment.githubUser; + + constructor(private http: HttpClient) { } + + getProjects(): Observable { + return this.http.get(this.apiReposString()).pipe( + map((projects: any) => { + return projects.map((project: any) => { + return { + name: project.name, + description: project.description, + license: project.license?.key, + link: project.html_url, + + stars: project.stargazers_count, + forks: project.forks_count, + watchers: project.watchers_count + } as Project; + }).filter((project: Project) => { + return project.name !== this.GITHUB_USER; + }); + }), + switchMap((projects: Project[]) => { + return new Observable((observer) => { + projects.forEach((project: Project, index: number) => { + this.getProjectLanguage(project).subscribe((languages: Language[]) => { + project.languages = languages; + observer.next(project); + }); + }); + }); + }) + ) + } + + private getProjectLanguage(project: Project): Observable { + return this.http.get(this.apiRepoLanguagesString(project.name)).pipe( + map((languages: any) => { + let totalBytes = 0; + Object.keys(languages).forEach((language: string) => { + totalBytes += languages[language]; + }); + + return Object.keys(languages).map((language: string) => { + return { + name: language, + percentage: (languages[language]/totalBytes)*100 + } as Language; + }); + }) + ); + } + + private apiReposString() { + return `${this.GITHUB_API_URL}/users/${this.GITHUB_USER}/repos`; + } + + private apiRepoLanguagesString(repoName: string) { + return `${this.GITHUB_API_URL}/repos/${this.GITHUB_USER}/${repoName}/languages`; + } +} diff --git a/src/app/shared/model/project/project.model.ts b/src/app/shared/model/project/project.model.ts new file mode 100644 index 0000000..8fd9c7b --- /dev/null +++ b/src/app/shared/model/project/project.model.ts @@ -0,0 +1,17 @@ +export type Language = { + name: string; + percentage: number; +} + +export type Project = { + name: string; + description: string; + link: string; + + license?: string; + languages?: Language[]; + + stars: number; + forks: number; + watchers: number; +} diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index d1643c6..adff1c3 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -2,4 +2,5 @@ export const environment = { production: true, backendPath: (window)['env']['BACKEND_URL'], backendOAuthPath: (window)['env']['BACKEND_OAUTH_URL'], + githubUser: (window)['env']['GITHUB_USER'], }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 6e0d313..bdf5548 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -6,6 +6,7 @@ export const environment = { production: false, backendPath: 'http://localhost:8070', backendOAuthPath: 'http://localhost:8070', + githubUser: 'HideyoshiNakazone', }; /*