diff --git a/package-lock.json b/package-lock.json index 8c704cd..b38dea8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 734b21f..ca82e65 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index f5671db..e410f49 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -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: ""} ]; diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 6a0720a..0395a20 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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] diff --git a/src/app/components/fancy-button/fancy-button.component.html b/src/app/components/fancy-button/fancy-button.component.html new file mode 100644 index 0000000..ab215c5 --- /dev/null +++ b/src/app/components/fancy-button/fancy-button.component.html @@ -0,0 +1,4 @@ + + {{label}} + {{label}} + diff --git a/src/app/components/fancy-button/fancy-button.component.scss b/src/app/components/fancy-button/fancy-button.component.scss new file mode 100644 index 0000000..37b00b6 --- /dev/null +++ b/src/app/components/fancy-button/fancy-button.component.scss @@ -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); + } +} diff --git a/src/app/components/fancy-button/fancy-button.component.ts b/src/app/components/fancy-button/fancy-button.component.ts new file mode 100644 index 0000000..3488a95 --- /dev/null +++ b/src/app/components/fancy-button/fancy-button.component.ts @@ -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; + +} diff --git a/src/app/components/project/project.component.html b/src/app/components/project/project.component.html new file mode 100644 index 0000000..0fdd158 --- /dev/null +++ b/src/app/components/project/project.component.html @@ -0,0 +1,14 @@ +
+ {{project.name}} +

{{project.name}}

+
+
+ +
+ {{project.status}} +
+ {{project.description}} +
+ +
+
diff --git a/src/app/components/project/project.component.scss b/src/app/components/project/project.component.scss new file mode 100644 index 0000000..bcd67a2 --- /dev/null +++ b/src/app/components/project/project.component.scss @@ -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; + } +} diff --git a/src/app/components/project/project.component.ts b/src/app/components/project/project.component.ts new file mode 100644 index 0000000..e2b20d6 --- /dev/null +++ b/src/app/components/project/project.component.ts @@ -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; + +} diff --git a/src/app/components/technology/technology.component.html b/src/app/components/technology/technology.component.html new file mode 100644 index 0000000..ee8a342 --- /dev/null +++ b/src/app/components/technology/technology.component.html @@ -0,0 +1,7 @@ +
+
+

{{technology.name}}

+ {{getTechnologyLevelName(technology.level)}} +
+
+
diff --git a/src/app/components/technology/technology.component.scss b/src/app/components/technology/technology.component.scss new file mode 100644 index 0000000..72ee2f5 --- /dev/null +++ b/src/app/components/technology/technology.component.scss @@ -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); + } +} diff --git a/src/app/components/technology/technology.component.ts b/src/app/components/technology/technology.component.ts new file mode 100644 index 0000000..1037762 --- /dev/null +++ b/src/app/components/technology/technology.component.ts @@ -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); + } + +} diff --git a/src/app/models/Technology.ts b/src/app/models/Technology.ts deleted file mode 100644 index 92975f0..0000000 --- a/src/app/models/Technology.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface Technology { - name: string, - level: 1 | 2 | 3 -} diff --git a/src/app/models/project.ts b/src/app/models/project.ts index 50536a3..587e724 100644 --- a/src/app/models/project.ts +++ b/src/app/models/project.ts @@ -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 } diff --git a/src/app/models/technology.ts b/src/app/models/technology.ts new file mode 100644 index 0000000..c56d4ce --- /dev/null +++ b/src/app/models/technology.ts @@ -0,0 +1,6 @@ +export interface Technology { + name: string, + level?: 1 | 2 | 3, + featured?: boolean, + type?: string +} diff --git a/src/app/pipes/frameworks.pipe.ts b/src/app/pipes/frameworks.pipe.ts new file mode 100644 index 0000000..86b1f9b --- /dev/null +++ b/src/app/pipes/frameworks.pipe.ts @@ -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; + } + +} diff --git a/src/app/pipes/languages.pipe.ts b/src/app/pipes/languages.pipe.ts new file mode 100644 index 0000000..c7d62b7 --- /dev/null +++ b/src/app/pipes/languages.pipe.ts @@ -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; + } + +} diff --git a/src/app/pipes/skills.pipe.ts b/src/app/pipes/skills.pipe.ts new file mode 100644 index 0000000..0c55ebf --- /dev/null +++ b/src/app/pipes/skills.pipe.ts @@ -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; + } + +} diff --git a/src/app/services/animator.service.ts b/src/app/services/animator.service.ts new file mode 100644 index 0000000..b2cb835 --- /dev/null +++ b/src/app/services/animator.service.ts @@ -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"); + }); + }); + } + +} diff --git a/src/app/services/backend.service.ts b/src/app/services/backend.service.ts new file mode 100644 index 0000000..de00fb9 --- /dev/null +++ b/src/app/services/backend.service.ts @@ -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 { + 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 { + return await this.pb?.collection('technologies').getFullList(); + } + +} diff --git a/src/app/sites/home/home.component.html b/src/app/sites/home/home.component.html index 1d07ab9..a713adf 100644 --- a/src/app/sites/home/home.component.html +++ b/src/app/sites/home/home.component.html @@ -18,37 +18,24 @@

Projekte

alle ansehen -
-
- {{project.name}} -

{{project.name}}

- {{project.description}} - -
+
+

Technologien

- mehr erfahren -
-
-
-

{{technology.name}}

- {{getTechnologyLevelName(technology.level)}} -
-
-
+ mehr erfahren +
+

Über mich

- mehr erfahren -
-
+ mehr erfahren +
+

{{timestamp.date}}

{{timestamp.description}}
diff --git a/src/app/sites/home/home.component.scss b/src/app/sites/home/home.component.scss index effd28b..54c7e53 100644 --- a/src/app/sites/home/home.component.scss +++ b/src/app/sites/home/home.component.scss @@ -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; + opacity: 0; + } - 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; - } - - 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%; + } +} diff --git a/src/app/sites/home/home.component.ts b/src/app/sites/home/home.component.ts index 26b7131..88d50ae 100644 --- a/src/app/sites/home/home.component.ts +++ b/src/app/sites/home/home.component.ts @@ -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); } } diff --git a/src/app/sites/projects/projects.component.html b/src/app/sites/projects/projects.component.html new file mode 100644 index 0000000..0f12a88 --- /dev/null +++ b/src/app/sites/projects/projects.component.html @@ -0,0 +1,8 @@ +
+

Alle Projekte

+
+
+ +
+
+
diff --git a/src/app/sites/projects/projects.component.scss b/src/app/sites/projects/projects.component.scss new file mode 100644 index 0000000..60dd502 --- /dev/null +++ b/src/app/sites/projects/projects.component.scss @@ -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; + } + } +} diff --git a/src/app/sites/projects/projects.component.ts b/src/app/sites/projects/projects.component.ts new file mode 100644 index 0000000..6ae40fd --- /dev/null +++ b/src/app/sites/projects/projects.component.ts @@ -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(); + } + +} diff --git a/src/app/sites/technologies/technologies.component.html b/src/app/sites/technologies/technologies.component.html new file mode 100644 index 0000000..4fb79db --- /dev/null +++ b/src/app/sites/technologies/technologies.component.html @@ -0,0 +1,35 @@ +
+
+
+
+
+
+

Technologien in Projekten

+
+
+
+
+ +
+

Programmiersprachen

+
+ +
+
+ +
+

Frameworks

+
+ +
+
+ +
+

Zusätzliche Fähigkeiten

+
+
+
+

{{skill.name}}

+
+
+
diff --git a/src/app/sites/technologies/technologies.component.scss b/src/app/sites/technologies/technologies.component.scss new file mode 100644 index 0000000..08c4ef8 --- /dev/null +++ b/src/app/sites/technologies/technologies.component.scss @@ -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; + } +} diff --git a/src/app/sites/technologies/technologies.component.ts b/src/app/sites/technologies/technologies.component.ts new file mode 100644 index 0000000..3808b37 --- /dev/null +++ b/src/app/sites/technologies/technologies.component.ts @@ -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(); + } + +} diff --git a/src/index.html b/src/index.html index a194d7d..8a9c204 100644 --- a/src/index.html +++ b/src/index.html @@ -2,13 +2,14 @@ - Portfolio + Portfolio von Leon Hoppe + diff --git a/src/styles.scss b/src/styles.scss index 8c65148..d72ec50 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -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; + } +} diff --git a/src/theme.scss b/src/theme.scss index 32cabca..70b9cd7 100644 --- a/src/theme.scss +++ b/src/theme.scss @@ -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; + } + } +} diff --git a/tsconfig.json b/tsconfig.json index ed966d4..7c2195f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,7 @@ "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, - "strict": true, + "strict": false, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true,