finished technologies page + added animations
This commit is contained in:
71
package-lock.json
generated
71
package-lock.json
generated
@@ -20,8 +20,11 @@
|
||||
"@angular/platform-server": "^15.1.0",
|
||||
"@angular/router": "^15.1.0",
|
||||
"@nguniversal/express-engine": "^15.1.0",
|
||||
"@types/chart.js": "^2.9.37",
|
||||
"chart.js": "^4.2.1",
|
||||
"express": "^4.15.2",
|
||||
"ngx-device-detector": "^5.0.1",
|
||||
"pocketbase": "^0.11.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.12.0"
|
||||
@@ -2838,6 +2841,11 @@
|
||||
"@jridgewell/sourcemap-codec": "1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@kurkle/color": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
|
||||
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
|
||||
},
|
||||
"node_modules/@leichtgewicht/ip-codec": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
|
||||
@@ -3950,6 +3958,14 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chart.js": {
|
||||
"version": "2.9.37",
|
||||
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.37.tgz",
|
||||
"integrity": "sha512-9bosRfHhkXxKYfrw94EmyDQcdjMaQPkU1fH2tDxu8DWXxf1mjzWQAV4laJF51ZbC2ycYwNDvIm1rGez8Bug0vg==",
|
||||
"dependencies": {
|
||||
"moment": "^2.10.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
|
||||
@@ -5764,6 +5780,17 @@
|
||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.2.1.tgz",
|
||||
"integrity": "sha512-6YbpQ0nt3NovAgOzbkSSeeAQu/3za1319dPUQTXn9WcOpywM8rGKxJHrhS8V8xEkAlk8YhEfjbuAPfUyp6jIsw==",
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"pnpm": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
@@ -9902,6 +9929,14 @@
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
@@ -10897,6 +10932,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pocketbase": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.11.0.tgz",
|
||||
"integrity": "sha512-nCH3xZoE4oNSWwvjCGOUkuxrZYwzmzEtgW1QrlrV6yPsc9E4D86NDHqXVtnjX7nOLB91FxsiJkZhhlO725yH7g=="
|
||||
},
|
||||
"node_modules/portscanner": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz",
|
||||
@@ -15510,6 +15550,11 @@
|
||||
"@jridgewell/sourcemap-codec": "1.4.14"
|
||||
}
|
||||
},
|
||||
"@kurkle/color": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
|
||||
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
|
||||
},
|
||||
"@leichtgewicht/ip-codec": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
|
||||
@@ -16523,6 +16568,14 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/chart.js": {
|
||||
"version": "2.9.37",
|
||||
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.37.tgz",
|
||||
"integrity": "sha512-9bosRfHhkXxKYfrw94EmyDQcdjMaQPkU1fH2tDxu8DWXxf1mjzWQAV4laJF51ZbC2ycYwNDvIm1rGez8Bug0vg==",
|
||||
"requires": {
|
||||
"moment": "^2.10.2"
|
||||
}
|
||||
},
|
||||
"@types/connect": {
|
||||
"version": "3.4.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
|
||||
@@ -17979,6 +18032,14 @@
|
||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
||||
"dev": true
|
||||
},
|
||||
"chart.js": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.2.1.tgz",
|
||||
"integrity": "sha512-6YbpQ0nt3NovAgOzbkSSeeAQu/3za1319dPUQTXn9WcOpywM8rGKxJHrhS8V8xEkAlk8YhEfjbuAPfUyp6jIsw==",
|
||||
"requires": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
@@ -21150,6 +21211,11 @@
|
||||
"minimist": "^1.2.6"
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
@@ -21906,6 +21972,11 @@
|
||||
"find-up": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"pocketbase": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.11.0.tgz",
|
||||
"integrity": "sha512-nCH3xZoE4oNSWwvjCGOUkuxrZYwzmzEtgW1QrlrV6yPsc9E4D86NDHqXVtnjX7nOLB91FxsiJkZhhlO725yH7g=="
|
||||
},
|
||||
"portscanner": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz",
|
||||
|
||||
@@ -26,8 +26,11 @@
|
||||
"@angular/platform-server": "^15.1.0",
|
||||
"@angular/router": "^15.1.0",
|
||||
"@nguniversal/express-engine": "^15.1.0",
|
||||
"@types/chart.js": "^2.9.37",
|
||||
"chart.js": "^4.2.1",
|
||||
"express": "^4.15.2",
|
||||
"ngx-device-detector": "^5.0.1",
|
||||
"pocketbase": "^0.11.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.12.0"
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import {HomeComponent} from "./sites/home/home.component";
|
||||
import {ProjectsComponent} from "./sites/projects/projects.component";
|
||||
import {TechnologiesComponent} from "./sites/technologies/technologies.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{path: "", component: HomeComponent},
|
||||
{path: "projects", component: ProjectsComponent},
|
||||
{path: "technologies", component: TechnologiesComponent},
|
||||
{path: "**", pathMatch: "full", redirectTo: ""}
|
||||
];
|
||||
|
||||
|
||||
@@ -5,26 +5,42 @@ import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { NavigationComponent } from './components/navigation/navigation.component';
|
||||
import {MatSidenavModule} from "@angular/material/sidenav";
|
||||
import {MatIconModule} from "@angular/material/icon";
|
||||
import {MatButtonModule} from "@angular/material/button";
|
||||
import { HomeComponent } from './sites/home/home.component';
|
||||
import { FeaturedProjectsPipe } from './pipes/featured-projects.pipe';
|
||||
import { ProjectsComponent } from './sites/projects/projects.component';
|
||||
import {MatTooltipModule} from "@angular/material/tooltip";
|
||||
import { FancyButtonComponent } from './components/fancy-button/fancy-button.component';
|
||||
import { ProjectComponent } from './components/project/project.component';
|
||||
import { TechnologiesComponent } from './sites/technologies/technologies.component';
|
||||
import { TechnologyComponent } from './components/technology/technology.component';
|
||||
import { LanguagesPipe } from './pipes/languages.pipe';
|
||||
import { FrameworksPipe } from './pipes/frameworks.pipe';
|
||||
import { SkillsPipe } from './pipes/skills.pipe';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
NavigationComponent,
|
||||
HomeComponent,
|
||||
FeaturedProjectsPipe
|
||||
FeaturedProjectsPipe,
|
||||
ProjectsComponent,
|
||||
FancyButtonComponent,
|
||||
ProjectComponent,
|
||||
TechnologiesComponent,
|
||||
TechnologyComponent,
|
||||
LanguagesPipe,
|
||||
FrameworksPipe,
|
||||
SkillsPipe
|
||||
],
|
||||
imports: [
|
||||
BrowserModule.withServerTransition({appId: 'serverApp'}),
|
||||
AppRoutingModule,
|
||||
BrowserAnimationsModule,
|
||||
MatSidenavModule,
|
||||
MatIconModule,
|
||||
MatButtonModule
|
||||
MatButtonModule,
|
||||
MatTooltipModule
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<a href="{{link}}" class="button" target="_blank">
|
||||
<span class="text-1">{{label}}</span>
|
||||
<span class="text-2">{{label}}</span>
|
||||
</a>
|
||||
54
src/app/components/fancy-button/fancy-button.component.scss
Normal file
54
src/app/components/fancy-button/fancy-button.component.scss
Normal file
@@ -0,0 +1,54 @@
|
||||
$text-move: hover-text-move 300ms forwards ease-out;
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
text-align: center;
|
||||
|
||||
width: max-content;
|
||||
height: 40px;
|
||||
padding-inline: 15px;
|
||||
border: 1px solid #FFF;
|
||||
border-radius: 20px;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
|
||||
.text-1 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.text-2 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.text-1 {
|
||||
animation: hover-text 250ms forwards ease-out reverse, $text-move;
|
||||
}
|
||||
|
||||
.text-2 {
|
||||
animation: hover-text 300ms forwards ease-out, $text-move;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hover-text {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hover-text-move {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
}
|
||||
13
src/app/components/fancy-button/fancy-button.component.ts
Normal file
13
src/app/components/fancy-button/fancy-button.component.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {Component, Input} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'fancy-button',
|
||||
templateUrl: './fancy-button.component.html',
|
||||
styleUrls: ['./fancy-button.component.scss']
|
||||
})
|
||||
export class FancyButtonComponent {
|
||||
|
||||
@Input('href') link: string | undefined;
|
||||
@Input('label') label: string | undefined;
|
||||
|
||||
}
|
||||
14
src/app/components/project/project.component.html
Normal file
14
src/app/components/project/project.component.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<div class="project">
|
||||
<img src="{{project.cover}}" alt="{{project.name}}" class="cover" draggable="false">
|
||||
<h2 class="name">{{project.name}}</h2>
|
||||
<div class="info" *ngIf="showInfo == ''">
|
||||
<div class="languages">
|
||||
<i [matTooltip]="language.label" class="language devicon-{{language.class}}-{{language.suffix || 'plain'}}" *ngFor="let language of project?.languages"></i>
|
||||
</div>
|
||||
<span class="status">{{project.status}}</span>
|
||||
</div>
|
||||
<span class="description">{{project.description}}</span>
|
||||
<div class="buttons">
|
||||
<fancy-button *ngFor="let button of project?.buttons" [href]="button.link" [label]="button.text" />
|
||||
</div>
|
||||
</div>
|
||||
62
src/app/components/project/project.component.scss
Normal file
62
src/app/components/project/project.component.scss
Normal file
@@ -0,0 +1,62 @@
|
||||
@use "src/theme";
|
||||
@use "src/styles" as s;
|
||||
@use "sass:map";
|
||||
|
||||
.project {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
padding: 5%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 30px;
|
||||
background-color: map.get(theme.$background, 'background');
|
||||
position: relative;
|
||||
box-shadow: 0 0 40px -10px theme.$primary;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
inset: -1px;
|
||||
background: theme.$gradient-straight;
|
||||
z-index: -1;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: 190px;
|
||||
border-radius: 12px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-block: 10px;
|
||||
font-size: 25px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.status, .languages i, .description {
|
||||
color: theme.$desc-color;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.languages {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
margin-top: auto;
|
||||
}
|
||||
}
|
||||
14
src/app/components/project/project.component.ts
Normal file
14
src/app/components/project/project.component.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import {Project} from "../../models/project";
|
||||
|
||||
@Component({
|
||||
selector: 'app-project',
|
||||
templateUrl: './project.component.html',
|
||||
styleUrls: ['./project.component.scss']
|
||||
})
|
||||
export class ProjectComponent {
|
||||
|
||||
@Input('project') project: Project | undefined;
|
||||
@Input('showInfo') showInfo: "" | undefined;
|
||||
|
||||
}
|
||||
7
src/app/components/technology/technology.component.html
Normal file
7
src/app/components/technology/technology.component.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<div class="technology">
|
||||
<div class="tech-header">
|
||||
<h2 class="tech-name">{{technology.name}}</h2>
|
||||
<span class="tech-level">{{getTechnologyLevelName(technology.level)}}</span>
|
||||
</div>
|
||||
<div [class]="createTechProgressClasses(technology.level)" #slider></div>
|
||||
</div>
|
||||
55
src/app/components/technology/technology.component.scss
Normal file
55
src/app/components/technology/technology.component.scss
Normal file
@@ -0,0 +1,55 @@
|
||||
@use "src/theme";
|
||||
|
||||
.technology {
|
||||
.tech-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.tech-name {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.tech-level {
|
||||
align-self: flex-end;
|
||||
color: theme.$desc-color;
|
||||
}
|
||||
}
|
||||
|
||||
.tech-progress {
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background: theme.$gradient;
|
||||
box-shadow: 0 3px 10px 0.5px rgba(theme.$primary, 0.4);
|
||||
transition: width 200ms ease-out;
|
||||
width: 0;
|
||||
|
||||
&.level-1 {
|
||||
--width: 33%;
|
||||
}
|
||||
|
||||
&.level-2 {
|
||||
--width: 66%;
|
||||
}
|
||||
|
||||
&.level-3 {
|
||||
--width: 100%;
|
||||
}
|
||||
|
||||
&.in-view {
|
||||
animation: slider-in 500ms forwards ease-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slider-in {
|
||||
from {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
width: var(--width);
|
||||
}
|
||||
}
|
||||
39
src/app/components/technology/technology.component.ts
Normal file
39
src/app/components/technology/technology.component.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {AfterViewInit, Component, ElementRef, Input, ViewChild} from '@angular/core';
|
||||
import {Technology} from "../../models/technology";
|
||||
import {AnimatorService} from "../../services/animator.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-technology',
|
||||
templateUrl: './technology.component.html',
|
||||
styleUrls: ['./technology.component.scss']
|
||||
})
|
||||
export class TechnologyComponent implements AfterViewInit {
|
||||
|
||||
@Input('technology') technology: Technology;
|
||||
@ViewChild('slider') slider: ElementRef;
|
||||
|
||||
public constructor(private animator: AnimatorService) {}
|
||||
|
||||
public getTechnologyLevelName(level: number): string {
|
||||
switch (level) {
|
||||
case 1:
|
||||
return "Anfänger";
|
||||
case 2:
|
||||
return "Erweitert";
|
||||
case 3:
|
||||
return "Fortgeschritten";
|
||||
|
||||
default:
|
||||
return "Normal";
|
||||
}
|
||||
}
|
||||
|
||||
public createTechProgressClasses(level: number): string {
|
||||
return `tech-progress level-${level}`;
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.animator.observer.observe(this.slider.nativeElement);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface Technology {
|
||||
name: string,
|
||||
level: 1 | 2 | 3
|
||||
}
|
||||
@@ -4,4 +4,12 @@ export interface Project {
|
||||
description: string;
|
||||
buttons?: {text: string; link: string}[];
|
||||
featured?: boolean;
|
||||
languages?: Language[];
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface Language {
|
||||
label: string,
|
||||
class: string,
|
||||
suffix?: string
|
||||
}
|
||||
|
||||
6
src/app/models/technology.ts
Normal file
6
src/app/models/technology.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface Technology {
|
||||
name: string,
|
||||
level?: 1 | 2 | 3,
|
||||
featured?: boolean,
|
||||
type?: string
|
||||
}
|
||||
18
src/app/pipes/frameworks.pipe.ts
Normal file
18
src/app/pipes/frameworks.pipe.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import {Technology} from "../models/technology";
|
||||
|
||||
@Pipe({
|
||||
name: 'frameworks'
|
||||
})
|
||||
export class FrameworksPipe implements PipeTransform {
|
||||
|
||||
transform(objects: Technology[]): Technology[] {
|
||||
const newObjects: Technology[] = [];
|
||||
objects?.forEach(obj => {
|
||||
if (obj?.type == "Framework")
|
||||
newObjects.push(obj);
|
||||
})
|
||||
return newObjects;
|
||||
}
|
||||
|
||||
}
|
||||
18
src/app/pipes/languages.pipe.ts
Normal file
18
src/app/pipes/languages.pipe.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import {Technology} from "../models/technology";
|
||||
|
||||
@Pipe({
|
||||
name: 'languages'
|
||||
})
|
||||
export class LanguagesPipe implements PipeTransform {
|
||||
|
||||
transform(objects: Technology[]): Technology[] {
|
||||
const newObjects: Technology[] = [];
|
||||
objects?.forEach(obj => {
|
||||
if (obj?.type == "Language")
|
||||
newObjects.push(obj);
|
||||
})
|
||||
return newObjects;
|
||||
}
|
||||
|
||||
}
|
||||
18
src/app/pipes/skills.pipe.ts
Normal file
18
src/app/pipes/skills.pipe.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import {Technology} from "../models/technology";
|
||||
|
||||
@Pipe({
|
||||
name: 'skills'
|
||||
})
|
||||
export class SkillsPipe implements PipeTransform {
|
||||
|
||||
transform(objects: Technology[]): Technology[] {
|
||||
const newObjects: Technology[] = [];
|
||||
objects?.forEach(obj => {
|
||||
if (obj?.type == "Additional")
|
||||
newObjects.push(obj);
|
||||
})
|
||||
return newObjects;
|
||||
}
|
||||
|
||||
}
|
||||
19
src/app/services/animator.service.ts
Normal file
19
src/app/services/animator.service.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AnimatorService {
|
||||
|
||||
public observer: IntersectionObserver;
|
||||
|
||||
public constructor() {
|
||||
this.observer = new IntersectionObserver(entries => {
|
||||
entries.forEach(entry => {
|
||||
if (!entry.isIntersecting) return;
|
||||
entry.target.classList.add("in-view");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
49
src/app/services/backend.service.ts
Normal file
49
src/app/services/backend.service.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import PocketBase from 'pocketbase';
|
||||
import {Language, Project} from "../models/project";
|
||||
import {Technology} from "../models/technology";
|
||||
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class BackendService {
|
||||
|
||||
private pb: PocketBase;
|
||||
|
||||
constructor() {
|
||||
this.pb = new PocketBase('https://ed168214-77da-44f1-9a61-859abb49edf8.api.leon-hoppe.de');
|
||||
}
|
||||
|
||||
public async getProjects(): Promise<Project[]> {
|
||||
const rawProjects = await this.pb?.collection('projects').getFullList(200, {
|
||||
sort: '-order'
|
||||
}) as Project[];
|
||||
const allLanguages = await this.pb?.collection('languages').getFullList();
|
||||
const states = await this.pb?.collection('project_states').getFullList();
|
||||
|
||||
const projects: Project[] = [];
|
||||
for(let rawProject of rawProjects) {
|
||||
const project = rawProject as Project;
|
||||
|
||||
project.status = states?.filter(state => state.id == rawProject.status)[0]['name'];
|
||||
|
||||
if (rawProject.languages != undefined) {
|
||||
const languages: Language[] = []
|
||||
for (let languageId of rawProject.languages as unknown as string[]) {
|
||||
languages.push(allLanguages?.filter(lang => lang.id == languageId)[0] as unknown as Language)
|
||||
}
|
||||
project.languages = languages;
|
||||
}
|
||||
|
||||
projects.push(project);
|
||||
}
|
||||
|
||||
return projects;
|
||||
}
|
||||
|
||||
public async getTechnologies(): Promise<Technology[]> {
|
||||
return await this.pb?.collection('technologies').getFullList();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,37 +18,24 @@
|
||||
<section id="projects" class="home-section" [ngClass]="{'mobile': deviceService.isMobile()}">
|
||||
<h1 class="title">Projekte</h1>
|
||||
<a routerLink="/projects">alle ansehen</a>
|
||||
<div id="project-wrapper">
|
||||
<div class="project" *ngFor="let project of projects | featuredProjects">
|
||||
<img src="{{project.cover}}" alt="{{project.name}}">
|
||||
<h2>{{project.name}}</h2>
|
||||
<span>{{project.description}}</span>
|
||||
<div class="project-buttons">
|
||||
<a class="project-button" *ngFor="let button of project.buttons" href="{{button.link}}">{{button.text}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="projects-wrapper" #projectsWrapper>
|
||||
<app-project *ngFor="let project of projects | featuredProjects; let i = index" [project]="project" [ngStyle]="{'animation-delay': getAnimationDelay(i)}" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="technologies" class="home-section" [ngClass]="{'mobile': deviceService.isMobile()}">
|
||||
<h1 class="title">Technologien</h1>
|
||||
<a href="/technologies">mehr erfahren</a>
|
||||
<div id="technology-wrapper">
|
||||
<div class="technology" *ngFor="let technology of technologies">
|
||||
<div class="tech-header">
|
||||
<h2 class="tech-name">{{technology.name}}</h2>
|
||||
<span class="tech-level">{{getTechnologyLevelName(technology.level)}}</span>
|
||||
</div>
|
||||
<div [class]="createTechProgressClasses(technology.level)"></div>
|
||||
</div>
|
||||
<a routerLink="/technologies">mehr erfahren</a>
|
||||
<div class="technologies-wrapper">
|
||||
<app-technology *ngFor="let technology of technologies" [technology]="technology" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="about" class="home-section" [ngClass]="{'mobile': deviceService.isMobile()}">
|
||||
<h1 class="title">Über mich</h1>
|
||||
<a href="/about">mehr erfahren</a>
|
||||
<div id="timeline">
|
||||
<div class="timestamp" *ngFor="let timestamp of timeline">
|
||||
<a routerLink="/about">mehr erfahren</a>
|
||||
<div id="timeline" #timelineElement>
|
||||
<div class="timestamp" *ngFor="let timestamp of timeline; let i = index" [ngStyle]="{'--delay': getAnimationDelay(i, 500)}">
|
||||
<h2>{{timestamp.date}}</h2>
|
||||
<span>{{timestamp.description}}</span>
|
||||
</div>
|
||||
|
||||
@@ -2,38 +2,9 @@
|
||||
@use "src/styles" as s;
|
||||
@use 'sass:map';
|
||||
|
||||
$gradient: linear-gradient(90deg, theme.$primary, theme.$secondary);
|
||||
$gradient-angled: linear-gradient(135deg, theme.$primary, theme.$secondary);
|
||||
$gradient-straight: linear-gradient(theme.$primary, theme.$secondary);
|
||||
|
||||
$padding: 12.5vw;
|
||||
$padding-small: 5vw;
|
||||
|
||||
$desc-color: #7c8393;
|
||||
|
||||
.title {
|
||||
font-size: 35px;
|
||||
display: inline;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.home-section {
|
||||
padding-inline: $padding;
|
||||
user-select: none;
|
||||
margin-bottom: 100px;
|
||||
|
||||
&.mobile {
|
||||
padding-inline: $padding-small;
|
||||
|
||||
.title {
|
||||
font-size: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#hero {
|
||||
height: 100vh;
|
||||
padding-left: $padding;
|
||||
padding-left: theme.$padding;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
@@ -45,7 +16,7 @@ $desc-color: #7c8393;
|
||||
line-height:70px;
|
||||
|
||||
span {
|
||||
background: $gradient;
|
||||
background: theme.$gradient;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
@@ -53,7 +24,7 @@ $desc-color: #7c8393;
|
||||
|
||||
p {
|
||||
font-size: 18px;
|
||||
color: $desc-color;
|
||||
color: theme.$desc-color;
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -61,7 +32,7 @@ $desc-color: #7c8393;
|
||||
margin-top: 40px;
|
||||
height: 60px;
|
||||
width: 150px;
|
||||
background: $gradient;
|
||||
background: theme.$gradient;
|
||||
border-radius: 30px;
|
||||
font-size: 15px;
|
||||
text-align: center;
|
||||
@@ -71,58 +42,8 @@ $desc-color: #7c8393;
|
||||
box-shadow: 0 0 40px -5px theme.$primary;
|
||||
}
|
||||
|
||||
.artwork {
|
||||
position: absolute;
|
||||
left: 55%;
|
||||
top: 19vh;
|
||||
|
||||
.circle {
|
||||
position: absolute;
|
||||
aspect-ratio: 1 / 1;
|
||||
z-index: -1;
|
||||
background: $gradient-angled;
|
||||
border-radius: 50%;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 1px;
|
||||
background-color: map.get(theme.$background, 'background');
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.big-circle {
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.small-circle {
|
||||
top: 100px;
|
||||
left: 350px;
|
||||
width: 150px;
|
||||
|
||||
&:after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
top: -50px;
|
||||
left: 170px;
|
||||
width: 250px;
|
||||
|
||||
&:after {
|
||||
background-image: url("/favicon.ico");
|
||||
background-size: 112%;
|
||||
background-position: -15px -15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
padding-left: $padding-small;
|
||||
padding-left: theme.$padding-small;
|
||||
|
||||
h1 {
|
||||
margin-top: 10vh;
|
||||
@@ -142,143 +63,33 @@ $desc-color: #7c8393;
|
||||
}
|
||||
|
||||
#projects {
|
||||
#project-wrapper {
|
||||
#projects-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 70px;
|
||||
justify-content: space-between;
|
||||
justify-content: space-evenly;
|
||||
gap: 70px;
|
||||
|
||||
.project {
|
||||
app-project {
|
||||
width: 400px;
|
||||
height: 500px;
|
||||
padding: 25px;
|
||||
box-sizing: border-box;
|
||||
|
||||
background: map.get(theme.$background, 'background');
|
||||
border-radius: 30px;
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
box-shadow: 0 0 40px -10px theme.$primary;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
content: '';
|
||||
background: $gradient-straight;
|
||||
border-radius: 30px;
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 350px;
|
||||
height: 170px;
|
||||
object-fit: cover;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-block: 10px 5px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: $desc-color;
|
||||
}
|
||||
|
||||
.project-buttons {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
margin-top: auto;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
|
||||
.project-button {
|
||||
display: block;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
|
||||
width: max-content;
|
||||
height: 40px;
|
||||
padding-inline: 15px;
|
||||
border: 1px solid #FFF;
|
||||
border-radius: 20px;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.in-view app-project {
|
||||
animation: fade-in 250ms forwards;
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
#project-wrapper {
|
||||
#projects-wrapper {
|
||||
margin-top: 30px;
|
||||
gap: 30px;
|
||||
|
||||
.project {
|
||||
width: 100%;
|
||||
height: 450px;
|
||||
box-shadow: none;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#technologies {
|
||||
margin-top: 300px;
|
||||
|
||||
#technology-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
margin-top: 40px;
|
||||
|
||||
.technology {
|
||||
.tech-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.tech-name {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.tech-level {
|
||||
align-self: flex-end;
|
||||
color: $desc-color;
|
||||
}
|
||||
}
|
||||
|
||||
.tech-progress {
|
||||
height: 10px;
|
||||
width: 100%;
|
||||
border-radius: 5px;
|
||||
background: $gradient;
|
||||
box-shadow: 0 3px 10px 0.5px rgba(theme.$primary, 0.4);
|
||||
|
||||
&.level-1 {
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
&.level-2 {
|
||||
width: 66%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#about {
|
||||
@@ -295,14 +106,16 @@ $desc-color: #7c8393;
|
||||
flex-direction: column;
|
||||
gap: 50px;
|
||||
position: relative;
|
||||
opacity: 0;
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
span {
|
||||
color: $desc-color;
|
||||
color: theme.$desc-color;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -310,7 +123,7 @@ $desc-color: #7c8393;
|
||||
content: '';
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background: $gradient-angled;
|
||||
background: theme.$gradient-angled;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 3px 10px 0.5px rgba(theme.$primary, 0.4);
|
||||
|
||||
@@ -320,7 +133,7 @@ $desc-color: #7c8393;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
width: 100%;
|
||||
width: 0;
|
||||
height: 3px;
|
||||
background-color: #FFF;
|
||||
top: 51px;
|
||||
@@ -331,6 +144,14 @@ $desc-color: #7c8393;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.in-view .timestamp {
|
||||
animation: fade-in 200ms forwards var(--delay) ease-out;
|
||||
|
||||
&:before {
|
||||
animation: timestamp-in 500ms forwards var(--delay) ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
@@ -341,6 +162,8 @@ $desc-color: #7c8393;
|
||||
gap: 15px;
|
||||
padding-left: 30px;
|
||||
box-sizing: border-box;
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
|
||||
span {
|
||||
margin-bottom: 50px;
|
||||
@@ -356,6 +179,7 @@ $desc-color: #7c8393;
|
||||
left: 1px;
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
&:last-of-type:before {
|
||||
@@ -379,7 +203,7 @@ $desc-color: #7c8393;
|
||||
user-select: unset;
|
||||
|
||||
.footer-title {
|
||||
background: $gradient;
|
||||
background: theme.$gradient;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
font-weight: bold;
|
||||
@@ -407,3 +231,13 @@ $desc-color: #7c8393;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes timestamp-in {
|
||||
from {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,23 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core';
|
||||
import {DeviceDetectorService} from "ngx-device-detector";
|
||||
import {Project} from "../../models/project";
|
||||
import {Technology} from "../../models/Technology";
|
||||
import {Technology} from "../../models/technology";
|
||||
import {BackendService} from "../../services/backend.service";
|
||||
import {AnimatorService} from "../../services/animator.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
templateUrl: './home.component.html',
|
||||
styleUrls: ['./home.component.scss']
|
||||
})
|
||||
export class HomeComponent {
|
||||
export class HomeComponent implements OnInit, AfterViewInit {
|
||||
|
||||
public constructor(public deviceService: DeviceDetectorService) {}
|
||||
@ViewChild('projectsWrapper') projectsWrapper: ElementRef;
|
||||
@ViewChild('timelineElement') timelineElement: ElementRef;
|
||||
public projects: Project[];
|
||||
public technologies: Technology[];
|
||||
|
||||
public projects: Project[] = [
|
||||
{
|
||||
name: "Test Project",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aspernatur excepturi facere, fuga maxime nulla qui voluptas voluptates? Adipisci asperiores dolor error iste sunt tempore. Blanditiis illum mollitia nostrum quae vero?",
|
||||
cover: "https://cdn.leon-hoppe.de/portfolio/projects/manager.jpeg",
|
||||
featured: true,
|
||||
buttons: [{
|
||||
text: "Source Code",
|
||||
link: "#hero"
|
||||
}]
|
||||
},
|
||||
{
|
||||
name: "Test Project",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aspernatur excepturi facere, fuga maxime nulla qui voluptas voluptates? Adipisci asperiores dolor error iste sunt tempore. Blanditiis illum mollitia nostrum quae vero?",
|
||||
cover: "https://cdn.leon-hoppe.de/portfolio/projects/manager.jpeg",
|
||||
featured: true,
|
||||
buttons: [{
|
||||
text: "Source Code",
|
||||
link: ""
|
||||
},
|
||||
{
|
||||
text: "gskjghjshfkafsdgs",
|
||||
link: "#hero"
|
||||
},]
|
||||
},
|
||||
{
|
||||
name: "Test Project",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aspernatur excepturi facere, fuga maxime nulla qui voluptas voluptates? Adipisci asperiores dolor error iste sunt tempore. Blanditiis illum mollitia nostrum quae vero?",
|
||||
cover: "https://cdn.leon-hoppe.de/portfolio/projects/manager.jpeg",
|
||||
featured: true
|
||||
},
|
||||
{
|
||||
name: "Test Project",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aspernatur excepturi facere, fuga maxime nulla qui voluptas voluptates? Adipisci asperiores dolor error iste sunt tempore. Blanditiis illum mollitia nostrum quae vero?",
|
||||
cover: "https://cdn.leon-hoppe.de/portfolio/projects/manager.jpeg",
|
||||
featured: true
|
||||
},
|
||||
];
|
||||
|
||||
public technologies: Technology[] = [
|
||||
{name: "C#", level: 3},
|
||||
{name: "Java", level: 3},
|
||||
{name: "HTML, CSS / SCSS", level: 2},
|
||||
{name: "JavaScript, TypeScript", level: 3},
|
||||
{name: "Lua", level: 2},
|
||||
{name: "Python", level: 1},
|
||||
];
|
||||
public constructor(public deviceService: DeviceDetectorService, private backend: BackendService, private animator: AnimatorService) {}
|
||||
|
||||
public timeline: {date: number, description: string}[] = [
|
||||
{date: 2010, description: "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aspernatur excepturi facere, fuga maxime nulla qui voluptas voluptates? Adipisci asperiores dolor error iste sunt tempore. Blanditiis illum mollitia nostrum quae vero?"},
|
||||
@@ -73,22 +32,18 @@ export class HomeComponent {
|
||||
{href: 'mailto://leon@ladenbau-hoppe.de', image: 'https://webmail.strato.de/favicon.ico'}
|
||||
];
|
||||
|
||||
public getTechnologyLevelName(level: number): string {
|
||||
switch (level) {
|
||||
case 1:
|
||||
return "Anfänger";
|
||||
case 2:
|
||||
return "Erweitert";
|
||||
case 3:
|
||||
return "Fortgeschritten";
|
||||
|
||||
default:
|
||||
return "Normal";
|
||||
}
|
||||
public getAnimationDelay(index: number, multiplier = 150): string {
|
||||
return `${index * multiplier}ms`;
|
||||
}
|
||||
|
||||
public createTechProgressClasses(level: number): string {
|
||||
return `tech-progress level-${level}`;
|
||||
async ngOnInit() {
|
||||
this.projects = await this.backend.getProjects();
|
||||
this.technologies = (await this.backend.getTechnologies()).filter(tech => tech.featured);
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.animator.observer.observe(this.projectsWrapper.nativeElement);
|
||||
this.animator.observer.observe(this.timelineElement.nativeElement);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
8
src/app/sites/projects/projects.component.html
Normal file
8
src/app/sites/projects/projects.component.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<section class="home-section" [ngClass]="{'mobile': deviceService.isMobile()}">
|
||||
<h1 class="title">Alle Projekte</h1>
|
||||
<div class="projects-wrapper">
|
||||
<div class="project-wrapper" *ngFor="let project of projects; let i = index" [ngStyle]="{'animation-delay': getAnimationDelay(i)}">
|
||||
<app-project [project]="project" showInfo />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
17
src/app/sites/projects/projects.component.scss
Normal file
17
src/app/sites/projects/projects.component.scss
Normal file
@@ -0,0 +1,17 @@
|
||||
.home-section {
|
||||
margin-top: 50px;
|
||||
|
||||
.projects-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 70px;
|
||||
justify-content: space-evenly;
|
||||
gap: 70px;
|
||||
|
||||
.project-wrapper {
|
||||
opacity: 0;
|
||||
animation: fade-in 250ms forwards;
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/app/sites/projects/projects.component.ts
Normal file
25
src/app/sites/projects/projects.component.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {Project} from "../../models/project";
|
||||
import {DeviceDetectorService} from "ngx-device-detector";
|
||||
import {BackendService} from "../../services/backend.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-projects',
|
||||
templateUrl: './projects.component.html',
|
||||
styleUrls: ['./projects.component.scss']
|
||||
})
|
||||
export class ProjectsComponent implements OnInit {
|
||||
|
||||
public constructor(public deviceService: DeviceDetectorService, private backend: BackendService) {}
|
||||
|
||||
public projects: Project[] | undefined;
|
||||
|
||||
public getAnimationDelay(index: number): string {
|
||||
return `${index * 150}ms`;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.projects = await this.backend.getProjects();
|
||||
}
|
||||
|
||||
}
|
||||
35
src/app/sites/technologies/technologies.component.html
Normal file
35
src/app/sites/technologies/technologies.component.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<section class="home-section" id="tech-projects" [ngClass]="{'mobile': deviceService.isMobile()}">
|
||||
<div class="artwork">
|
||||
<div class="circle big-circle"></div>
|
||||
<div class="circle small-circle"></div>
|
||||
<div class="circle image"></div>
|
||||
</div>
|
||||
<h1 class="title">Technologien in Projekten</h1>
|
||||
<div class="chart">
|
||||
<div class="chart-container"><canvas #chard></canvas></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="home-section" id="languages" [ngClass]="{'mobile': deviceService.isMobile()}">
|
||||
<h1 class="title">Programmiersprachen</h1>
|
||||
<div class="technologies-wrapper">
|
||||
<app-technology *ngFor="let technology of technologies | languages" [technology]="technology" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="home-section" id="frameworks" [ngClass]="{'mobile': deviceService.isMobile()}">
|
||||
<h1 class="title">Frameworks</h1>
|
||||
<div class="technologies-wrapper">
|
||||
<app-technology *ngFor="let technology of technologies | frameworks" [technology]="technology" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="home-section" id="additional" [ngClass]="{'mobile': deviceService.isMobile()}">
|
||||
<h1 class="title">Zusätzliche Fähigkeiten</h1>
|
||||
<div id="skills-wrapper">
|
||||
<div class="skill" *ngFor="let skill of technologies | skills">
|
||||
<div class="dot"></div>
|
||||
<h2>{{skill.name}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
75
src/app/sites/technologies/technologies.component.scss
Normal file
75
src/app/sites/technologies/technologies.component.scss
Normal file
@@ -0,0 +1,75 @@
|
||||
@use "src/theme";
|
||||
@use "sass:map";
|
||||
|
||||
#tech-projects {
|
||||
margin-top: 200px;
|
||||
position: relative;
|
||||
|
||||
.chart {
|
||||
display: flex;
|
||||
margin-top: 30px;
|
||||
|
||||
.chart-container {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
.artwork {
|
||||
top: 40px;
|
||||
}
|
||||
|
||||
@media (screen and max-width: 1300px) {
|
||||
.artwork {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chart {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
margin-top: 50px;
|
||||
|
||||
.chart .chart-container {
|
||||
aspect-ratio: 1 / 1;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#additional {
|
||||
#skills-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
gap: 20px;
|
||||
margin-top: 30px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.skill {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
|
||||
.dot {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background: theme.$gradient;
|
||||
box-shadow: 0 3px 10px 0.5px rgba(theme.$primary, 0.4);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile #skills-wrapper {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
65
src/app/sites/technologies/technologies.component.ts
Normal file
65
src/app/sites/technologies/technologies.component.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core';
|
||||
import Chart from 'chart.js/auto';
|
||||
import {BackendService} from "../../services/backend.service";
|
||||
import {Technology} from "../../models/technology";
|
||||
import {DeviceDetectorService} from "ngx-device-detector";
|
||||
|
||||
@Component({
|
||||
selector: 'app-technologies',
|
||||
templateUrl: './technologies.component.html',
|
||||
styleUrls: ['./technologies.component.scss']
|
||||
})
|
||||
export class TechnologiesComponent implements AfterViewInit {
|
||||
|
||||
@ViewChild('chard') chartRef: ElementRef;
|
||||
public technologies: Technology[];
|
||||
|
||||
public constructor(public deviceService: DeviceDetectorService, private backend: BackendService) {}
|
||||
|
||||
async ngAfterViewInit() {
|
||||
const projects = await this.backend.getProjects();
|
||||
const data: {lang: string, count: number}[] = [];
|
||||
|
||||
for (let project of projects) {
|
||||
for (let lang of project.languages) {
|
||||
if (lang.class == "angularjs") {
|
||||
project.languages.push({label: "HTML", class: ""}, {label: "CSS", class: ""}, {label: "TypeScript", class: ""});
|
||||
}
|
||||
|
||||
if (lang.class == "dotnetcore") {
|
||||
project.languages.push({label: "C#", class: ''});
|
||||
}
|
||||
|
||||
let element = data.filter(row => row.lang == lang.label)[0];
|
||||
if (element == undefined) {
|
||||
element = {lang: lang.label, count: 0};
|
||||
data.push(element);
|
||||
}
|
||||
|
||||
|
||||
|
||||
element.count++;
|
||||
}
|
||||
}
|
||||
|
||||
Chart.defaults.color = "#FFF";
|
||||
new Chart(
|
||||
this.chartRef.nativeElement,
|
||||
{
|
||||
type: "doughnut",
|
||||
data: {
|
||||
labels: data.map(row => row.lang),
|
||||
datasets: [
|
||||
{
|
||||
label: "Projekte",
|
||||
data: data.map(row => row.count)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.technologies = await this.backend.getTechnologies();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,13 +2,14 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Portfolio</title>
|
||||
<title>Portfolio von Leon Hoppe</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/gh/devicons/devicon@v2.15.1/devicon.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="mat-typography">
|
||||
<app-root></app-root>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
@use "sass:map";
|
||||
@use "theme";
|
||||
|
||||
html, body { height: 100vh; }
|
||||
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
|
||||
@@ -43,3 +44,70 @@ mat-drawer > div {
|
||||
@function css-clamp( $values... ) {
|
||||
@return css-function( clamp, $values );
|
||||
}
|
||||
|
||||
.technologies-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.artwork {
|
||||
position: absolute;
|
||||
left: 55%;
|
||||
top: 19vh;
|
||||
|
||||
.circle {
|
||||
position: absolute;
|
||||
aspect-ratio: 1 / 1;
|
||||
z-index: -1;
|
||||
background: theme.$gradient-angled;
|
||||
border-radius: 50%;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 1px;
|
||||
background-color: map.get(theme.$background, 'background');
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.big-circle {
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.small-circle {
|
||||
top: 100px;
|
||||
left: 350px;
|
||||
width: 150px;
|
||||
|
||||
&:after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
top: -50px;
|
||||
left: 170px;
|
||||
width: 250px;
|
||||
|
||||
&:after {
|
||||
background-image: url("/favicon.ico");
|
||||
background-size: 112%;
|
||||
background-position: -15px -15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,15 @@ $angular-theme: modify-background($angular-theme, #0f1724);
|
||||
$primary: #8e5bd2;
|
||||
$secondary: #11a8bd;
|
||||
|
||||
$gradient: linear-gradient(90deg, $primary, $secondary);
|
||||
$gradient-angled: linear-gradient(135deg, $primary, $secondary);
|
||||
$gradient-straight: linear-gradient($primary, $secondary);
|
||||
|
||||
$padding: 12.5vw;
|
||||
$padding-small: 5vw;
|
||||
|
||||
$desc-color: #7c8393;
|
||||
|
||||
$color-config: mat.get-color-config($angular-theme);
|
||||
$background: map.get($color-config, 'background');
|
||||
$text: map.get($color-config, 'foreground');
|
||||
@@ -55,3 +64,23 @@ body {
|
||||
* {
|
||||
color: map.get($text, 'text');
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 35px;
|
||||
display: inline;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.home-section {
|
||||
padding-inline: $padding;
|
||||
user-select: none;
|
||||
margin-bottom: 100px;
|
||||
|
||||
&.mobile {
|
||||
padding-inline: $padding-small;
|
||||
|
||||
.title {
|
||||
font-size: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"baseUrl": "./",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"strict": false,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
|
||||
Reference in New Issue
Block a user