diff --git a/.github/workflows/vercel-cleanup-pr.yml b/.github/workflows/vercel-cleanup-pr.yml new file mode 100644 index 0000000..082202f --- /dev/null +++ b/.github/workflows/vercel-cleanup-pr.yml @@ -0,0 +1,27 @@ +name: vercel-cleanup-pr + +on: + pull_request: + types: [closed] + +env: + VERCEL_CLI_TOKEN: ${{ secrets.VERCEL_CLI_TOKEN }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + GITHUB_PR_ID: ${{ github.event.number }} + +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - name: Cleanup Vercel Deployments + run: | + closed_deployments=$(curl "https://api.vercel.com/v6/deployments?projectId=$VERCEL_PROJECT_ID" \ + -H "Accept: application/json" \ + -H "Authorization: Bearer ${VERCEL_CLI_TOKEN}" | jq -r ".deployments[] | select(.meta.githubPrId == \"${GITHUB_PR_ID}\") | .uid") + for deployment in $closed_deployments; do + echo "Deleting Deployment: $deployment" + curl "https://api.vercel.com/v6/now/deployments/$deployment" \ + -X DELETE \ + -H "Authorization: Bearer ${VERCEL_CLI_TOKEN}" + done + diff --git a/.github/workflows/vercel-cleanup-previous-preview.yml b/.github/workflows/vercel-cleanup-previous-preview.yml new file mode 100644 index 0000000..0906850 --- /dev/null +++ b/.github/workflows/vercel-cleanup-previous-preview.yml @@ -0,0 +1,33 @@ +name: vercel-cleanup-preview + +on: + push: + branches: + - '*' + - '!main' + - '!devel' + +env: + VERCEL_CLI_TOKEN: ${{ secrets.VERCEL_CLI_TOKEN }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + GIT_PREVIOS_COMMIT: ${{ github.event.before }} + + +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Cleanup Vercel Deployments + run: | + + invalid_deployments=$(curl "https://api.vercel.com/v6/deployments?projectId=$VERCEL_PROJECT_ID" \ + -H "Accept: application/json" \ + -H "Authorization: Bearer ${VERCEL_CLI_TOKEN}" | jq -r ".deployments[] | select(.meta.githubCommitSha == \"${GIT_PREVIOS_COMMIT}\") | .uid") + + for deployment in $invalid_deployments; do + echo "Deleting Deployment: $deployment" + curl "https://api.vercel.com/v6/now/deployments/$deployment" \ + -X DELETE \ + -H "Authorization: Bearer ${VERCEL_CLI_TOKEN}" + done diff --git a/.gitignore b/.gitignore index 4fcb0ba..d04aabd 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ yarn-error.log .project .classpath .c9/ +.nx/ *.launch .settings/ *.sublime-workspace @@ -45,3 +46,5 @@ Thumbs.db src/assets/env.js .env + +.secret diff --git a/package-lock.json b/package-lock.json index c9e31e2..b343608 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,12 +25,14 @@ "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@glidejs/glide": "^3.6.0", + "apexcharts": "^3.45.1", "bootstrap": "^4.6.2", "cookieconsent": "^3.1.1", "cors": "^2.8.5", "envsub": "^4.1.0", "express": "^4.18.1", "jquery": "^3.6.0", + "ng-apexcharts": "^1.8.0", "ngx-cookie-service": "^16.0.1", "ngx-cookieconsent": "^4.0.2", "ngx-glide": "^16.0.0", @@ -5440,6 +5442,11 @@ "node": ">=14.15.0" } }, + "node_modules/@yr/monotone-cubic-spline": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", + "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==" + }, "node_modules/@zkochan/js-yaml": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz", @@ -5722,6 +5729,20 @@ "node": ">= 8" } }, + "node_modules/apexcharts": { + "version": "3.45.1", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.45.1.tgz", + "integrity": "sha512-pPjj/SA6dfPvR/IKRZF0STdfBGpBh3WRt7K0DFuW9P8erypYkX17EHu3/molPRfo2zSiQwTVpshHC5ncysqfkA==", + "dependencies": { + "@yr/monotone-cubic-spline": "^1.0.3", + "svg.draggable.js": "^2.2.2", + "svg.easing.js": "^2.0.0", + "svg.filter.js": "^2.0.2", + "svg.pathmorphing.js": "^0.1.3", + "svg.resize.js": "^1.4.3", + "svg.select.js": "^3.0.1" + } + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -12063,6 +12084,20 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node_modules/ng-apexcharts": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/ng-apexcharts/-/ng-apexcharts-1.8.0.tgz", + "integrity": "sha512-NwJuMLHoLm52LSzM08RXV6oOOTyUYREAV53WHVGs+L2qi8UWbxCz19hX0kk+F/xFLEhhuiLegO3T1v30jLbKSQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": ">=13.0.0", + "@angular/core": ">=13.0.0", + "apexcharts": "^3.41.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/ngx-cookie-service": { "version": "16.0.1", "resolved": "https://registry.npmjs.org/ngx-cookie-service/-/ngx-cookie-service-16.0.1.tgz", @@ -15233,6 +15268,89 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg.draggable.js": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz", + "integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==", + "dependencies": { + "svg.js": "^2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.easing.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz", + "integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==", + "dependencies": { + "svg.js": ">=2.3.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.filter.js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz", + "integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==", + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz", + "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==" + }, + "node_modules/svg.pathmorphing.js": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz", + "integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==", + "dependencies": { + "svg.js": "^2.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz", + "integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==", + "dependencies": { + "svg.js": "^2.6.5", + "svg.select.js": "^2.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js/node_modules/svg.select.js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz", + "integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==", + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.select.js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz", + "integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==", + "dependencies": { + "svg.js": "^2.6.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", diff --git a/package.json b/package.json index 0b5c8bd..2646c9c 100644 --- a/package.json +++ b/package.json @@ -33,12 +33,14 @@ "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@glidejs/glide": "^3.6.0", + "apexcharts": "^3.45.1", "bootstrap": "^4.6.2", "cookieconsent": "^3.1.1", "cors": "^2.8.5", "envsub": "^4.1.0", "express": "^4.18.1", "jquery": "^3.6.0", + "ng-apexcharts": "^1.8.0", "ngx-cookie-service": "^16.0.1", "ngx-cookieconsent": "^4.0.2", "ngx-glide": "^16.0.0", diff --git a/src/app/app-router.module.ts b/src/app/app-router.module.ts index 43d8625..1f099fc 100644 --- a/src/app/app-router.module.ts +++ b/src/app/app-router.module.ts @@ -1,9 +1,9 @@ import { NgModule } from '@angular/core'; 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'; + const routes: Routes = [ { path: '', @@ -12,7 +12,11 @@ const routes: Routes = [ }, { path: 'home', - component: HomeComponent, + loadChildren: () => import('./home/home.module').then(mod => mod.HomeModule), + }, + { + path: 'projects', + loadChildren: () => import('./projects/projects.module').then(mod => mod.ProjectsModule), }, { path: 'callback', diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 2b4186e..66f2d6e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,5 +1,5 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { AuthService } from './shared/auth/auth.service'; +import { AuthService } from './shared/service/auth.service'; import { UpdateService } from './shared/service-worker/update.service'; import { NgcCookieConsentService, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 1ee9eb6..3c70d55 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -4,21 +4,18 @@ import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; import { HeaderModule } from './header/header.module'; import { SharedModule } from './shared/shared.module'; -import { HomeComponent } from './home/home.component'; import { AppRouterModule } from './app-router.module'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { AppServiceWorkerModule } from './app-service-worker.module'; import { ServiceWorkerModule } from '@angular/service-worker'; import { environment } from '../environments/environment'; import { FooterComponent } from './footer/footer.component'; -import {HomeModule} from "./home/home.module"; @NgModule({ declarations: [AppComponent, FooterComponent], imports: [ BrowserModule, HeaderModule, - HomeModule, AppRouterModule, AppServiceWorkerModule, SharedModule, diff --git a/src/app/header/header-dropdown/header-dropdown.component.ts b/src/app/header/header-dropdown/header-dropdown.component.ts index 2812b9b..55aac63 100644 --- a/src/app/header/header-dropdown/header-dropdown.component.ts +++ b/src/app/header/header-dropdown/header-dropdown.component.ts @@ -22,7 +22,7 @@ import { faUser, } from '@fortawesome/free-solid-svg-icons'; import { Subscription } from 'rxjs'; -import { AuthService } from 'src/app/shared/auth/auth.service'; +import { AuthService } from 'src/app/shared/service/auth.service'; import { User } from '../../shared/model/user/user.model'; import UserChecker from '../../shared/model/user/user.checker'; import { HelpComponent } from '../header-popup/help/help.component'; @@ -96,25 +96,25 @@ export class HeaderDropdownComponent implements OnInit, OnDestroy { private userSubscription!: Subscription; @Input() - state: boolean = false; + state: boolean = false; @Input() - ignoreClickOutside!: HTMLDivElement[]; + ignoreClickOutside!: HTMLDivElement[]; @Output() - clickOutside = new EventEmitter(); + clickOutside = new EventEmitter(); @Output() - loginPopupState: EventEmitter = new EventEmitter(); + loginPopupState: EventEmitter = new EventEmitter(); @Output() - signupPopupState: EventEmitter = new EventEmitter(); + signupPopupState: EventEmitter = new EventEmitter(); @Output() - helpPopupState: EventEmitter = new EventEmitter(); + helpPopupState: EventEmitter = new EventEmitter(); @Output() - myProfilePopupState: EventEmitter = new EventEmitter(); + myProfilePopupState: EventEmitter = new EventEmitter(); constructor( private viewContainerRef: ViewContainerRef, diff --git a/src/app/header/header-popup/callback/callback.component.ts b/src/app/header/header-popup/callback/callback.component.ts index 18bc37f..3a81d79 100644 --- a/src/app/header/header-popup/callback/callback.component.ts +++ b/src/app/header/header-popup/callback/callback.component.ts @@ -1,6 +1,6 @@ import { ActivatedRoute, Router } from '@angular/router'; import { Component, OnInit } from '@angular/core'; -import { AuthService } from 'src/app/shared/auth/auth.service'; +import { AuthService } from 'src/app/shared/service/auth.service'; @Component({ selector: 'app-callback', @@ -19,15 +19,15 @@ export class CallbackComponent implements OnInit { let auth: 'google' | 'github' = p['auth']; switch (auth) { - case 'github': - this.authService.loginGithubUser(p); - break; - case 'google': - this.authService.loginGoogleUser(p); - break; - default: - console.log(`Unimplemented auth: ${auth}`); - break; + case 'github': + this.authService.loginGithubUser(p); + break; + case 'google': + this.authService.loginGoogleUser(p); + break; + default: + console.log(`Unimplemented auth: ${auth}`); + break; } this.router.navigate(['/home']); diff --git a/src/app/header/header-popup/login/login.component.ts b/src/app/header/header-popup/login/login.component.ts index f96873e..a6a1baa 100644 --- a/src/app/header/header-popup/login/login.component.ts +++ b/src/app/header/header-popup/login/login.component.ts @@ -13,7 +13,7 @@ import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; import { faLock, faUser } from '@fortawesome/free-solid-svg-icons'; import { Subscription } from 'rxjs'; -import { AuthService } from 'src/app/shared/auth/auth.service'; +import { AuthService } from 'src/app/shared/service/auth.service'; import { HttpError } from 'src/app/shared/model/httpError/httpError.model'; import HttpErrorChecker from 'src/app/shared/model/httpError/httpErrorChecker'; import UserChecker from 'src/app/shared/model/user/user.checker'; @@ -99,13 +99,13 @@ const GITHUB_LOGO_SVG = 'assets/img/providers/github.svg'; }) export class LoginComponent implements OnInit, AfterViewInit, OnDestroy { @Input() - state: boolean = false; + state: boolean = false; @Input() - ignoreClickOutside!: HTMLDivElement[]; + ignoreClickOutside!: HTMLDivElement[]; @Output() - stateChange = new EventEmitter(); + stateChange = new EventEmitter(); loginForm!: FormGroup; diff --git a/src/app/header/header-popup/my-profile/my-profile.component.ts b/src/app/header/header-popup/my-profile/my-profile.component.ts index ee46287..fe3bdf0 100644 --- a/src/app/header/header-popup/my-profile/my-profile.component.ts +++ b/src/app/header/header-popup/my-profile/my-profile.component.ts @@ -6,7 +6,7 @@ import { OnInit, Output, } from '@angular/core'; -import { AuthService } from '../../../shared/auth/auth.service'; +import { AuthService } from '../../../shared/service/auth.service'; import { User } from '../../../shared/model/user/user.model'; import { animate, @@ -88,16 +88,16 @@ import { faFileUpload } from '@fortawesome/free-solid-svg-icons'; }) export class MyProfileComponent implements OnInit { @Input() - state: boolean = false; + state: boolean = false; @Input() - user!: User | null; + user!: User | null; @Input() - ignoreClickOutside!: HTMLDivElement[]; + ignoreClickOutside!: HTMLDivElement[]; @Output() - stateChange = new EventEmitter(); + stateChange = new EventEmitter(); alterForm!: FormGroup; diff --git a/src/app/header/header-popup/my-profile/profile-picture-picker/profile-picture-picker.component.ts b/src/app/header/header-popup/my-profile/profile-picture-picker/profile-picture-picker.component.ts index e211ac5..62e89e2 100644 --- a/src/app/header/header-popup/my-profile/profile-picture-picker/profile-picture-picker.component.ts +++ b/src/app/header/header-popup/my-profile/profile-picture-picker/profile-picture-picker.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Output } from '@angular/core'; -import { AuthService } from '../../../../shared/auth/auth.service'; +import { AuthService } from '../../../../shared/service/auth.service'; @Component({ selector: 'app-profile-picture-picker', @@ -8,7 +8,7 @@ import { AuthService } from '../../../../shared/auth/auth.service'; }) export class ProfilePicturePickerComponent { @Output() - imageSent = new EventEmitter(); + imageSent = new EventEmitter(); private profilePicture!: File; diff --git a/src/app/header/header-popup/signup/signup.component.ts b/src/app/header/header-popup/signup/signup.component.ts index 6c041b3..3430aca 100644 --- a/src/app/header/header-popup/signup/signup.component.ts +++ b/src/app/header/header-popup/signup/signup.component.ts @@ -9,7 +9,7 @@ import { faUser, } from '@fortawesome/free-solid-svg-icons'; import { Subscription } from 'rxjs'; -import { AuthService } from 'src/app/shared/auth/auth.service'; +import { AuthService } from 'src/app/shared/service/auth.service'; import { HttpError } from 'src/app/shared/model/httpError/httpError.model'; import HttpErrorChecker from 'src/app/shared/model/httpError/httpErrorChecker'; import UserChecker from 'src/app/shared/model/user/user.checker'; @@ -90,13 +90,13 @@ const GITHUB_LOGO_SVG = 'assets/img/providers/github.svg'; }) export class SignupComponent implements OnInit { @Input() - state: boolean = false; + state: boolean = false; @Input() - ignoreClickOutside!: HTMLDivElement[]; + ignoreClickOutside!: HTMLDivElement[]; @Output() - stateChange = new EventEmitter(); + stateChange = new EventEmitter(); signupForm!: FormGroup; diff --git a/src/app/header/header-slider/nav-slider/nav-slider.component.ts b/src/app/header/header-slider/nav-slider/nav-slider.component.ts index b2b65c9..3154012 100644 --- a/src/app/header/header-slider/nav-slider/nav-slider.component.ts +++ b/src/app/header/header-slider/nav-slider/nav-slider.component.ts @@ -10,7 +10,7 @@ import { faUser } from '@fortawesome/free-solid-svg-icons'; import { SliderItemComponent } from 'src/app/shared/components/slider-item/slider-item.component'; import UserChecker from '../../../shared/model/user/user.checker'; import { User } from '../../../shared/model/user/user.model'; -import { AuthService } from '../../../shared/auth/auth.service'; +import { AuthService } from '../../../shared/service/auth.service'; import { Subscription } from 'rxjs'; @Component({ @@ -25,14 +25,14 @@ export class NavSliderComponent userIcon = faUser; @Input() - pages!: { name: string; route: string }[]; + pages!: { name: string; route: string }[]; loggedUser!: User | null; private userSubscription!: Subscription; @Output() - profileButtonClicked = new EventEmitter(); + profileButtonClicked = new EventEmitter(); constructor(private authService: AuthService) { super(); diff --git a/src/app/header/header-slider/user-slider/user-slider.component.ts b/src/app/header/header-slider/user-slider/user-slider.component.ts index f97c412..9da3b36 100644 --- a/src/app/header/header-slider/user-slider/user-slider.component.ts +++ b/src/app/header/header-slider/user-slider/user-slider.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, OnInit, Output } from '@angular/core'; import { Subscription } from 'rxjs'; -import { AuthService } from 'src/app/shared/auth/auth.service'; +import { AuthService } from 'src/app/shared/service/auth.service'; import { SliderItemComponent } from 'src/app/shared/components/slider-item/slider-item.component'; import UserChecker from 'src/app/shared/model/user/user.checker'; import { User } from 'src/app/shared/model/user/user.model'; @@ -52,16 +52,16 @@ export class UserSliderComponent extends SliderItemComponent implements OnInit { authSubscription!: Subscription; @Output() - loginPopupState: EventEmitter = new EventEmitter(); + loginPopupState: EventEmitter = new EventEmitter(); @Output() - signupPopupState: EventEmitter = new EventEmitter(); + signupPopupState: EventEmitter = new EventEmitter(); @Output() - helpPopupState: EventEmitter = new EventEmitter(); + helpPopupState: EventEmitter = new EventEmitter(); @Output() - myProfilePopupState: EventEmitter = new EventEmitter(); + myProfilePopupState: EventEmitter = new EventEmitter(); constructor(private authService: AuthService) { super(); diff --git a/src/app/header/header.component.ts b/src/app/header/header.component.ts index 4f75f3f..e9f0b64 100644 --- a/src/app/header/header.component.ts +++ b/src/app/header/header.component.ts @@ -10,7 +10,7 @@ import { import { faUser } from '@fortawesome/free-solid-svg-icons'; import { LoginComponent } from './header-popup/login/login.component'; import { SignupComponent } from './header-popup/signup/signup.component'; -import { AuthService } from '../shared/auth/auth.service'; +import { AuthService } from '../shared/service/auth.service'; import UserChecker from '../shared/model/user/user.checker'; import { User } from '../shared/model/user/user.model'; import { Subscription } from 'rxjs'; @@ -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/home/home-router.module.ts b/src/app/home/home-router.module.ts new file mode 100644 index 0000000..7cfc348 --- /dev/null +++ b/src/app/home/home-router.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import {HomeComponent} from "./home.component"; + +const routes: Routes = [ + { + path: '', + component: HomeComponent, + }, +]; + +@NgModule({ + declarations: [], + imports: [CommonModule, RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class HomeRouterModule {} diff --git a/src/app/home/home.module.ts b/src/app/home/home.module.ts index 2bfa1be..96d951b 100644 --- a/src/app/home/home.module.ts +++ b/src/app/home/home.module.ts @@ -4,6 +4,7 @@ import {HomeComponent} from "./home.component"; import {StackSliderComponent} from "./stack-slider/stack-slider.component"; import {StackCardComponent} from "./stack-slider/stack-card/stack-card.component"; import {NgxGlideComponent} from "ngx-glide"; +import {HomeRouterModule} from "./home-router.module"; @@ -16,7 +17,8 @@ import {NgxGlideComponent} from "ngx-glide"; imports: [ CommonModule, NgxGlideComponent, - NgOptimizedImage + NgOptimizedImage, + HomeRouterModule ] }) export class HomeModule { } diff --git a/src/app/home/stack-slider/stack-card/stack-card.component.css b/src/app/home/stack-slider/stack-card/stack-card.component.css index b3c5ebb..1b71ed7 100644 --- a/src/app/home/stack-slider/stack-card/stack-card.component.css +++ b/src/app/home/stack-slider/stack-card/stack-card.component.css @@ -14,6 +14,10 @@ display: flex; align-items: center; } +.stack-card-image::selection, +.stack-card-image *::selection { + background: transparent; +} .stack-card-body { padding: 10px; 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..23fc3c6 --- /dev/null +++ b/src/app/projects/project-card/project-card.component.css @@ -0,0 +1,94 @@ +.card .inverse-card-image { + flex-direction: row-reverse; +} + +.card .card-content { + 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 a { + font-size: 2rem; + font-weight: 600; + justify-content: left; + color: #767676; + text-decoration: none; +} + +.card .card-content .card-text { + font-size: 1.2rem; + font-weight: 400; + color: #919294; +} + + +.card .card-content .card-content-f { + display: flex; + justify-content: center; + flex-direction: row; +} +.card .card-content .card-content-f .card-stats { + display: flex; + flex-direction: row; + justify-content: space-around; +} + + +.stat-item { + margin: 5px 0px 5px 0px; + display: flex; + flex-direction: row; + align-items: start; + align-content: start; + + color: #919294; +} +.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; +} + +.card-languages { + padding: 0; + margin-top: 25px; + margin-bottom: 25px; +} + +/* COMPUTER FORMAT */ +@media only screen and (min-width: 770px) { + .card-languages { + margin: 0; + } + + .card .card-content .card-content-f .card-stats { + display: flex; + flex-direction: column; + justify-content: space-around; + } +} 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..668f584 --- /dev/null +++ b/src/app/projects/project-card/project-card.component.html @@ -0,0 +1,49 @@ +
+
+
+

+ {{project.name}} +

+

{{project.description}}

+
+
+
+ + +
+
+
+
+ +
+ {{project.license}} +
+
+
+ +
+ {{project.stars}} +
+
+
+ +
+ {{project.forks}} +
+
+
+ +
+ {{project.watchers}} +
+
+
+
+
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..45b5ad3 --- /dev/null +++ b/src/app/projects/project-card/project-card.component.ts @@ -0,0 +1,114 @@ +import {Component, HostListener, Input, OnInit, ViewChild} from '@angular/core'; +import {faCodeFork, faEye, faScaleBalanced, faStar} from '@fortawesome/free-solid-svg-icons'; +import {Language, Project} from "../../shared/model/project/project.model"; +import { + ApexChart, ApexDataLabels, + ApexNonAxisChartSeries, + ApexPlotOptions, + ApexResponsive, + ChartComponent +} from "ng-apexcharts"; + + +export type ChartOptions = { + series: ApexNonAxisChartSeries; + colors: string[]; + chart: ApexChart; + responsive: ApexResponsive[]; + labels: string[]; + plotOptions: ApexPlotOptions; + dataLabels: ApexDataLabels; +}; + +@Component({ + selector: 'app-project-card', + templateUrl: './project-card.component.html', + styleUrls: ['./project-card.component.css'] +}) +export class ProjectCardComponent implements OnInit { + @Input() inverted: boolean = false; + + @Input() project!: Project; + + @ViewChild('language-chart') + languageChart: ChartComponent | undefined; + + chartOptions: ChartOptions | undefined; + + // Stats Icons Definitions + faLicense = faScaleBalanced; + + faStars = faStar; + + faCodeFork = faCodeFork; + + faEye = faEye; + + private windowResizeTimeout: any; + + ngOnInit() { + if (!!this.project.languages) { + const windowWidth = window.innerWidth; + this.chartOptions = this.generateChart(this.project.languages, windowWidth); + } + + } + + @HostListener('window:resize', ['$event']) + getScreenSize(event: Event) { + clearTimeout(this.windowResizeTimeout); + + this.windowResizeTimeout = setTimeout(() => { + if (!this.project.languages) return; + this.chartOptions = this.generateChart( + this.project.languages, window.innerWidth + ); + }, 100); + } + + get hasLicense(): boolean { + return this.project.license !== undefined; + } + + get hasLanguage(): boolean { + return this.project.languages !== undefined && + this.project.languages?.length > 0; + } + + private generateChart(languages: Language[], windowWidth: number): ChartOptions { + const responsiveWindowWidth = windowWidth >= 530 ? + 300 : (windowWidth*.8 - 80); + + return { + series: languages.map(value => value.percentage), + colors: languages.map(value => value.color), + chart: { + width: 380, + type: "donut" + }, + labels: languages.map(value => value.name), + responsive: [ + { + breakpoint: 530, + options: { + chart: { + width: responsiveWindowWidth + }, + legend: { + position: "bottom" + } + } + } + ], + plotOptions: { + pie: { + expandOnClick: true, + + } + }, + dataLabels: { + enabled: false + } + }; + } +} diff --git a/src/app/projects/projects-router.module.ts b/src/app/projects/projects-router.module.ts new file mode 100644 index 0000000..6f01310 --- /dev/null +++ b/src/app/projects/projects-router.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import {ProjectsComponent} from "./projects.component"; + +const routes: Routes = [ + { + path: '', + component: ProjectsComponent + }, +]; + +@NgModule({ + declarations: [], + imports: [CommonModule, RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class ProjectsRouterModule {} 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.ts b/src/app/projects/projects.component.ts new file mode 100644 index 0000000..2412c35 --- /dev/null +++ b/src/app/projects/projects.component.ts @@ -0,0 +1,27 @@ +import {Component, OnInit} from '@angular/core'; +import {GithubService} from "../shared/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..15b5a90 --- /dev/null +++ b/src/app/projects/projects.module.ts @@ -0,0 +1,25 @@ +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"; +import {NgApexchartsModule} from "ng-apexcharts"; +import {ProjectsRouterModule} from "./projects-router.module"; + +@NgModule({ + declarations: [ + ProjectsComponent, + ProjectCardComponent + ], + imports: [ + CommonModule, + NgOptimizedImage, + MatIconModule, + FontAwesomeModule, + NgApexchartsModule, + ProjectsRouterModule + ], + exports: [] +}) +export class ProjectsModule { } 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..af47098 --- /dev/null +++ b/src/app/shared/model/project/project.model.ts @@ -0,0 +1,18 @@ +export type Language = { + name: string; + color: 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/app/shared/auth/auth.service.ts b/src/app/shared/service/auth.service.ts similarity index 99% rename from src/app/shared/auth/auth.service.ts rename to src/app/shared/service/auth.service.ts index b3aedae..678376b 100644 --- a/src/app/shared/auth/auth.service.ts +++ b/src/app/shared/service/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/service/github.service.ts b/src/app/shared/service/github.service.ts new file mode 100644 index 0000000..90ba7d7 --- /dev/null +++ b/src/app/shared/service/github.service.ts @@ -0,0 +1,155 @@ +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, tap +} from 'rxjs'; +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class GithubService { + + GITHUB_API_URL = 'https://api.github.com'; + + GITHUB_API_COLORS = 'https://raw.githubusercontent.com/ozh/github-colors/master/colors.json'; + + GITHUB_USER = environment.githubUser; + + colors!: Map; + + constructor(private http: HttpClient) { + this.getLanguageColor().subscribe((colors: Map) => { + this.colors = colors; + }); + } + + getProjects(): Observable { + if (this.isLocalStorageValid()) { + console.log('Fetching projects from local storage') + return this.getProjectsFromLocalStorage(); + } + + console.log('Fetching projects from Github API') + return this.getProjectsFromGithub() + } + + private isLocalStorageValid(): boolean { + const projects = localStorage.getItem('github-projects'); + let status = !!projects + + if (!status) { + return false; + } + + const timestamp = localStorage.getItem('github-projects-timestamp'); + if (timestamp) { + const diff = new Date().getTime() - parseInt(timestamp); + status = diff < 86400000; + } else { + status = false; + } + + return status; + } + + private saveProjectsToLocalStorage(project: Project) { + const p = localStorage.getItem('github-projects'); + if (p) { + const projects = JSON.parse(p); + projects.push(project); + localStorage.setItem('github-projects', JSON.stringify(projects)); + } else { + localStorage.setItem('github-projects', JSON.stringify([project])); + } + + localStorage.setItem('github-projects-timestamp', new Date().getTime().toString()); + } + + private getProjectsFromLocalStorage(): Observable { + const projects = localStorage.getItem('github-projects') || '[]'; + return new Observable((observer) => { + JSON.parse(projects).forEach((project: Project) => { + observer.next(project); + }); + }); + } + + private getProjectsFromGithub(): 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); + }); + }); + }); + }), + tap((project: Project) => this.saveProjectsToLocalStorage(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, + color: this.colors.get(language) || this.getRandColor(), + percentage: (languages[language]/totalBytes)*100 + } as Language; + }); + }) + ); + } + + private getLanguageColor(): Observable> { + return this.http.get(this.GITHUB_API_COLORS).pipe( + map((colors: any) => { + const colorMap = new Map(); + Object.keys(colors).forEach((language: string) => { + colorMap.set(language, colors[language].color); + }); + return colorMap; + }) + ); + } + + private getRandColor(): string { + return `#${Math.floor(Math.random()*16777215).toString(16)}`; + } + + 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/assets/env.sample.js b/src/assets/env.sample.js index ac046df..151211c 100644 --- a/src/assets/env.sample.js +++ b/src/assets/env.sample.js @@ -4,4 +4,5 @@ // Environment variables window["env"]["BACKEND_URL"] = "${BACKEND_URL}"; window["env"]["BACKEND_OAUTH_URL"] = "${BACKEND_OAUTH_URL}"; + window["env"]["GITHUB_USER"] = "${GITHUB_USER}"; })(this); 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', }; /*