finished technologies page + added animations

This commit is contained in:
2023-02-19 20:55:08 +01:00
parent 0583b2256a
commit a55c60d88a
34 changed files with 890 additions and 303 deletions

71
package-lock.json generated
View File

@@ -20,8 +20,11 @@
"@angular/platform-server": "^15.1.0", "@angular/platform-server": "^15.1.0",
"@angular/router": "^15.1.0", "@angular/router": "^15.1.0",
"@nguniversal/express-engine": "^15.1.0", "@nguniversal/express-engine": "^15.1.0",
"@types/chart.js": "^2.9.37",
"chart.js": "^4.2.1",
"express": "^4.15.2", "express": "^4.15.2",
"ngx-device-detector": "^5.0.1", "ngx-device-detector": "^5.0.1",
"pocketbase": "^0.11.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"zone.js": "~0.12.0" "zone.js": "~0.12.0"
@@ -2838,6 +2841,11 @@
"@jridgewell/sourcemap-codec": "1.4.14" "@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": { "node_modules/@leichtgewicht/ip-codec": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
@@ -3950,6 +3958,14 @@
"@types/node": "*" "@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": { "node_modules/@types/connect": {
"version": "3.4.35", "version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
@@ -5764,6 +5780,17 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"dev": true "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": { "node_modules/chokidar": {
"version": "3.5.3", "version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
@@ -9902,6 +9929,14 @@
"mkdirp": "bin/cmd.js" "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": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -10897,6 +10932,11 @@
"node": ">=8" "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": { "node_modules/portscanner": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz", "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz",
@@ -15510,6 +15550,11 @@
"@jridgewell/sourcemap-codec": "1.4.14" "@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": { "@leichtgewicht/ip-codec": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
@@ -16523,6 +16568,14 @@
"@types/node": "*" "@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": { "@types/connect": {
"version": "3.4.35", "version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
@@ -17979,6 +18032,14 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"dev": true "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": { "chokidar": {
"version": "3.5.3", "version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
@@ -21150,6 +21211,11 @@
"minimist": "^1.2.6" "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": { "ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -21906,6 +21972,11 @@
"find-up": "^4.0.0" "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": { "portscanner": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz", "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz",

View File

@@ -26,8 +26,11 @@
"@angular/platform-server": "^15.1.0", "@angular/platform-server": "^15.1.0",
"@angular/router": "^15.1.0", "@angular/router": "^15.1.0",
"@nguniversal/express-engine": "^15.1.0", "@nguniversal/express-engine": "^15.1.0",
"@types/chart.js": "^2.9.37",
"chart.js": "^4.2.1",
"express": "^4.15.2", "express": "^4.15.2",
"ngx-device-detector": "^5.0.1", "ngx-device-detector": "^5.0.1",
"pocketbase": "^0.11.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"zone.js": "~0.12.0" "zone.js": "~0.12.0"

View File

@@ -1,9 +1,13 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import {HomeComponent} from "./sites/home/home.component"; import {HomeComponent} from "./sites/home/home.component";
import {ProjectsComponent} from "./sites/projects/projects.component";
import {TechnologiesComponent} from "./sites/technologies/technologies.component";
const routes: Routes = [ const routes: Routes = [
{path: "", component: HomeComponent}, {path: "", component: HomeComponent},
{path: "projects", component: ProjectsComponent},
{path: "technologies", component: TechnologiesComponent},
{path: "**", pathMatch: "full", redirectTo: ""} {path: "**", pathMatch: "full", redirectTo: ""}
]; ];

View File

@@ -5,26 +5,42 @@ import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NavigationComponent } from './components/navigation/navigation.component'; import { NavigationComponent } from './components/navigation/navigation.component';
import {MatSidenavModule} from "@angular/material/sidenav";
import {MatIconModule} from "@angular/material/icon"; import {MatIconModule} from "@angular/material/icon";
import {MatButtonModule} from "@angular/material/button"; import {MatButtonModule} from "@angular/material/button";
import { HomeComponent } from './sites/home/home.component'; import { HomeComponent } from './sites/home/home.component';
import { FeaturedProjectsPipe } from './pipes/featured-projects.pipe'; 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({ @NgModule({
declarations: [ declarations: [
AppComponent, AppComponent,
NavigationComponent, NavigationComponent,
HomeComponent, HomeComponent,
FeaturedProjectsPipe FeaturedProjectsPipe,
ProjectsComponent,
FancyButtonComponent,
ProjectComponent,
TechnologiesComponent,
TechnologyComponent,
LanguagesPipe,
FrameworksPipe,
SkillsPipe
], ],
imports: [ imports: [
BrowserModule.withServerTransition({appId: 'serverApp'}), BrowserModule.withServerTransition({appId: 'serverApp'}),
AppRoutingModule, AppRoutingModule,
BrowserAnimationsModule, BrowserAnimationsModule,
MatSidenavModule,
MatIconModule, MatIconModule,
MatButtonModule MatButtonModule,
MatTooltipModule
], ],
providers: [], providers: [],
bootstrap: [AppComponent] bootstrap: [AppComponent]

View File

@@ -0,0 +1,4 @@
<a href="{{link}}" class="button" target="_blank">
<span class="text-1">{{label}}</span>
<span class="text-2">{{label}}</span>
</a>

View 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);
}
}

View 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;
}

View 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>

View 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;
}
}

View 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;
}

View 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>

View 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);
}
}

View 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);
}
}

View File

@@ -1,4 +0,0 @@
export interface Technology {
name: string,
level: 1 | 2 | 3
}

View File

@@ -4,4 +4,12 @@ export interface Project {
description: string; description: string;
buttons?: {text: string; link: string}[]; buttons?: {text: string; link: string}[];
featured?: boolean; featured?: boolean;
languages?: Language[];
status?: string;
}
export interface Language {
label: string,
class: string,
suffix?: string
} }

View File

@@ -0,0 +1,6 @@
export interface Technology {
name: string,
level?: 1 | 2 | 3,
featured?: boolean,
type?: string
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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");
});
});
}
}

View 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();
}
}

View File

@@ -18,37 +18,24 @@
<section id="projects" class="home-section" [ngClass]="{'mobile': deviceService.isMobile()}"> <section id="projects" class="home-section" [ngClass]="{'mobile': deviceService.isMobile()}">
<h1 class="title">Projekte</h1> <h1 class="title">Projekte</h1>
<a routerLink="/projects">alle ansehen</a> <a routerLink="/projects">alle ansehen</a>
<div id="project-wrapper"> <div id="projects-wrapper" #projectsWrapper>
<div class="project" *ngFor="let project of projects | featuredProjects"> <app-project *ngFor="let project of projects | featuredProjects; let i = index" [project]="project" [ngStyle]="{'animation-delay': getAnimationDelay(i)}" />
<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> </div>
</section> </section>
<section id="technologies" class="home-section" [ngClass]="{'mobile': deviceService.isMobile()}"> <section id="technologies" class="home-section" [ngClass]="{'mobile': deviceService.isMobile()}">
<h1 class="title">Technologien</h1> <h1 class="title">Technologien</h1>
<a href="/technologies">mehr erfahren</a> <a routerLink="/technologies">mehr erfahren</a>
<div id="technology-wrapper"> <div class="technologies-wrapper">
<div class="technology" *ngFor="let technology of technologies"> <app-technology *ngFor="let technology of technologies" [technology]="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)"></div>
</div>
</div> </div>
</section> </section>
<section id="about" class="home-section" [ngClass]="{'mobile': deviceService.isMobile()}"> <section id="about" class="home-section" [ngClass]="{'mobile': deviceService.isMobile()}">
<h1 class="title">Über mich</h1> <h1 class="title">Über mich</h1>
<a href="/about">mehr erfahren</a> <a routerLink="/about">mehr erfahren</a>
<div id="timeline"> <div id="timeline" #timelineElement>
<div class="timestamp" *ngFor="let timestamp of timeline"> <div class="timestamp" *ngFor="let timestamp of timeline; let i = index" [ngStyle]="{'--delay': getAnimationDelay(i, 500)}">
<h2>{{timestamp.date}}</h2> <h2>{{timestamp.date}}</h2>
<span>{{timestamp.description}}</span> <span>{{timestamp.description}}</span>
</div> </div>

View File

@@ -2,38 +2,9 @@
@use "src/styles" as s; @use "src/styles" as s;
@use 'sass:map'; @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 { #hero {
height: 100vh; height: 100vh;
padding-left: $padding; padding-left: theme.$padding;
user-select: none; user-select: none;
position: relative; position: relative;
overflow-x: hidden; overflow-x: hidden;
@@ -45,7 +16,7 @@ $desc-color: #7c8393;
line-height:70px; line-height:70px;
span { span {
background: $gradient; background: theme.$gradient;
background-clip: text; background-clip: text;
color: transparent; color: transparent;
} }
@@ -53,7 +24,7 @@ $desc-color: #7c8393;
p { p {
font-size: 18px; font-size: 18px;
color: $desc-color; color: theme.$desc-color;
} }
a { a {
@@ -61,7 +32,7 @@ $desc-color: #7c8393;
margin-top: 40px; margin-top: 40px;
height: 60px; height: 60px;
width: 150px; width: 150px;
background: $gradient; background: theme.$gradient;
border-radius: 30px; border-radius: 30px;
font-size: 15px; font-size: 15px;
text-align: center; text-align: center;
@@ -71,58 +42,8 @@ $desc-color: #7c8393;
box-shadow: 0 0 40px -5px theme.$primary; 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 { &.mobile {
padding-left: $padding-small; padding-left: theme.$padding-small;
h1 { h1 {
margin-top: 10vh; margin-top: 10vh;
@@ -142,143 +63,33 @@ $desc-color: #7c8393;
} }
#projects { #projects {
#project-wrapper { #projects-wrapper {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
margin-top: 70px; margin-top: 70px;
justify-content: space-between; justify-content: space-evenly;
gap: 70px; gap: 70px;
.project { app-project {
width: 400px; width: 400px;
height: 500px; opacity: 0;
padding: 25px; }
box-sizing: border-box;
background: map.get(theme.$background, 'background'); &.in-view app-project {
border-radius: 30px; animation: fade-in 250ms forwards;
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;
}
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;
}
}
}
} }
} }
&.mobile { &.mobile {
#project-wrapper { #projects-wrapper {
margin-top: 30px; margin-top: 30px;
gap: 30px; gap: 30px;
.project {
width: 100%;
height: 450px;
box-shadow: none;
img {
width: 100%;
height: auto;
}
}
} }
} }
} }
#technologies { #technologies {
margin-top: 300px; 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 { #about {
@@ -295,14 +106,16 @@ $desc-color: #7c8393;
flex-direction: column; flex-direction: column;
gap: 50px; gap: 50px;
position: relative; position: relative;
opacity: 0;
h2 { h2 {
font-size: 20px; font-size: 20px;
margin: 0; margin: 0;
font-weight: normal;
} }
span { span {
color: $desc-color; color: theme.$desc-color;
font-size: 14px; font-size: 14px;
} }
@@ -310,7 +123,7 @@ $desc-color: #7c8393;
content: ''; content: '';
width: 15px; width: 15px;
height: 15px; height: 15px;
background: $gradient-angled; background: theme.$gradient-angled;
border-radius: 50%; border-radius: 50%;
box-shadow: 0 3px 10px 0.5px rgba(theme.$primary, 0.4); box-shadow: 0 3px 10px 0.5px rgba(theme.$primary, 0.4);
@@ -320,7 +133,7 @@ $desc-color: #7c8393;
&:before { &:before {
content: ''; content: '';
width: 100%; width: 0;
height: 3px; height: 3px;
background-color: #FFF; background-color: #FFF;
top: 51px; top: 51px;
@@ -331,6 +144,14 @@ $desc-color: #7c8393;
display: none; 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 { &.mobile {
@@ -341,6 +162,8 @@ $desc-color: #7c8393;
gap: 15px; gap: 15px;
padding-left: 30px; padding-left: 30px;
box-sizing: border-box; box-sizing: border-box;
animation: none;
opacity: 1;
span { span {
margin-bottom: 50px; margin-bottom: 50px;
@@ -356,6 +179,7 @@ $desc-color: #7c8393;
left: 1px; left: 1px;
width: 3px; width: 3px;
height: 100%; height: 100%;
animation: none;
} }
&:last-of-type:before { &:last-of-type:before {
@@ -379,7 +203,7 @@ $desc-color: #7c8393;
user-select: unset; user-select: unset;
.footer-title { .footer-title {
background: $gradient; background: theme.$gradient;
background-clip: text; background-clip: text;
color: transparent; color: transparent;
font-weight: bold; font-weight: bold;
@@ -407,3 +231,13 @@ $desc-color: #7c8393;
} }
} }
} }
@keyframes timestamp-in {
from {
width: 0;
}
to {
width: 100%;
}
}

View File

@@ -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 {DeviceDetectorService} from "ngx-device-detector";
import {Project} from "../../models/project"; 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({ @Component({
selector: 'app-home', selector: 'app-home',
templateUrl: './home.component.html', templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'] 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[] = [ public constructor(public deviceService: DeviceDetectorService, private backend: BackendService, private animator: AnimatorService) {}
{
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 timeline: {date: number, description: string}[] = [ 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?"}, {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'} {href: 'mailto://leon@ladenbau-hoppe.de', image: 'https://webmail.strato.de/favicon.ico'}
]; ];
public getTechnologyLevelName(level: number): string { public getAnimationDelay(index: number, multiplier = 150): string {
switch (level) { return `${index * multiplier}ms`;
case 1:
return "Anfänger";
case 2:
return "Erweitert";
case 3:
return "Fortgeschritten";
default:
return "Normal";
}
} }
public createTechProgressClasses(level: number): string { async ngOnInit() {
return `tech-progress level-${level}`; 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);
} }
} }

View 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>

View 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;
}
}
}

View 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();
}
}

View 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>

View 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;
}
}

View 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();
}
}

View File

@@ -2,13 +2,14 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Portfolio</title> <title>Portfolio von Leon Hoppe</title>
<base href="/"> <base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.gstatic.com"> <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/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://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> </head>
<body class="mat-typography"> <body class="mat-typography">
<app-root></app-root> <app-root></app-root>

View File

@@ -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; } html, body { height: 100vh; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
@@ -43,3 +44,70 @@ mat-drawer > div {
@function css-clamp( $values... ) { @function css-clamp( $values... ) {
@return css-function( 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;
}
}

View File

@@ -44,6 +44,15 @@ $angular-theme: modify-background($angular-theme, #0f1724);
$primary: #8e5bd2; $primary: #8e5bd2;
$secondary: #11a8bd; $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); $color-config: mat.get-color-config($angular-theme);
$background: map.get($color-config, 'background'); $background: map.get($color-config, 'background');
$text: map.get($color-config, 'foreground'); $text: map.get($color-config, 'foreground');
@@ -55,3 +64,23 @@ body {
* { * {
color: map.get($text, 'text'); 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;
}
}
}

View File

@@ -5,7 +5,7 @@
"baseUrl": "./", "baseUrl": "./",
"outDir": "./dist/out-tsc", "outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"strict": true, "strict": false,
"noImplicitOverride": true, "noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true, "noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true, "noImplicitReturns": true,