finished v1.0

This commit is contained in:
2023-02-24 20:37:52 +01:00
parent 484294e611
commit 4471157412
45 changed files with 638 additions and 350 deletions

14
.dockerignore Normal file
View File

@@ -0,0 +1,14 @@
# Ignore the node_modules directory
node_modules
# Ignore the dist directory
dist
# Ignore the .git directory
.git
# Ignore the .gitignore file
.gitignore
# Ignore the .dockerignore file
.dockerignore

11
Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
#stage 1
FROM node:18-slim as node
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build:ssr --omit=dev
#stage 2
FROM node:18-slim
COPY --from=node /app/dist /app/dist
WORKDIR /app
CMD ["node", "dist/Portfolio/server/main.js"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 479 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -27,7 +27,9 @@
"inlineStyleLanguage": "scss", "inlineStyleLanguage": "scss",
"assets": [ "assets": [
"src/favicon.ico", "src/favicon.ico",
"src/assets" "src/assets",
"src/robots.txt",
"src/sitemap.xml"
], ],
"styles": [ "styles": [
"src/styles.scss", "src/styles.scss",
@@ -40,13 +42,13 @@
"budgets": [ "budgets": [
{ {
"type": "initial", "type": "initial",
"maximumWarning": "500kb", "maximumWarning": "5mb",
"maximumError": "1mb" "maximumError": "10mb"
}, },
{ {
"type": "anyComponentStyle", "type": "anyComponentStyle",
"maximumWarning": "2kb", "maximumWarning": "100kb",
"maximumError": "4kb" "maximumError": "100kb"
} }
], ],
"outputHashing": "all" "outputHashing": "all"

47
package-lock.json generated
View File

@@ -20,13 +20,10 @@
"@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",
"@sweetalert2/theme-dark": "^5.0.15",
"chart.js": "^4.2.1", "chart.js": "^4.2.1",
"express": "^4.15.2", "express": "^4.15.2",
"ngx-device-detector": "^5.0.1",
"pocketbase": "^0.11.0", "pocketbase": "^0.11.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"sweetalert2": "^11.7.2",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"zone.js": "~0.12.0" "zone.js": "~0.12.0"
}, },
@@ -3933,11 +3930,6 @@
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==",
"dev": true "dev": true
}, },
"node_modules/@sweetalert2/theme-dark": {
"version": "5.0.15",
"resolved": "https://registry.npmjs.org/@sweetalert2/theme-dark/-/theme-dark-5.0.15.tgz",
"integrity": "sha512-g1QCwQVOkiAz5hIEBOIvvu0580lubu4KuQlod+48QetYzGIEXNlHEH36QihCDnGVgE6vx48iO48w9q0WrZWyHQ=="
},
"node_modules/@tootallnate/once": { "node_modules/@tootallnate/once": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
@@ -10036,18 +10028,6 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true "dev": true
}, },
"node_modules/ngx-device-detector": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ngx-device-detector/-/ngx-device-detector-5.0.1.tgz",
"integrity": "sha512-hVKaGzyXzy6zeliYyN7runz3eOOsh3tmZ8A6P5MSpHIjVjSx3pUJcobFTKNyHGn/zGS4JFWuhSSb7QmNwmqK9w==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"@angular/common": "^15.0.0",
"@angular/core": "^15.0.0"
}
},
"node_modules/nice-napi": { "node_modules/nice-napi": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz",
@@ -12453,15 +12433,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/sweetalert2": {
"version": "11.7.2",
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.7.2.tgz",
"integrity": "sha512-atPjDa3fv/4xwZpiAt7FZUgAhR5VAASiLP2hu7HUeVDXx+v4/9nD1W0u8xal1e9f2/qGh0DwTxPXPV9XoZIBvg==",
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/limonte"
}
},
"node_modules/symbol-observable": { "node_modules/symbol-observable": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
@@ -16562,11 +16533,6 @@
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==",
"dev": true "dev": true
}, },
"@sweetalert2/theme-dark": {
"version": "5.0.15",
"resolved": "https://registry.npmjs.org/@sweetalert2/theme-dark/-/theme-dark-5.0.15.tgz",
"integrity": "sha512-g1QCwQVOkiAz5hIEBOIvvu0580lubu4KuQlod+48QetYzGIEXNlHEH36QihCDnGVgE6vx48iO48w9q0WrZWyHQ=="
},
"@tootallnate/once": { "@tootallnate/once": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
@@ -21312,14 +21278,6 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true "dev": true
}, },
"ngx-device-detector": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ngx-device-detector/-/ngx-device-detector-5.0.1.tgz",
"integrity": "sha512-hVKaGzyXzy6zeliYyN7runz3eOOsh3tmZ8A6P5MSpHIjVjSx3pUJcobFTKNyHGn/zGS4JFWuhSSb7QmNwmqK9w==",
"requires": {
"tslib": "^2.0.0"
}
},
"nice-napi": { "nice-napi": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz",
@@ -23136,11 +23094,6 @@
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true "dev": true
}, },
"sweetalert2": {
"version": "11.7.2",
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.7.2.tgz",
"integrity": "sha512-atPjDa3fv/4xwZpiAt7FZUgAhR5VAASiLP2hu7HUeVDXx+v4/9nD1W0u8xal1e9f2/qGh0DwTxPXPV9XoZIBvg=="
},
"symbol-observable": { "symbol-observable": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",

View File

@@ -26,13 +26,10 @@
"@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",
"@sweetalert2/theme-dark": "^5.0.15",
"chart.js": "^4.2.1", "chart.js": "^4.2.1",
"express": "^4.15.2", "express": "^4.15.2",
"ngx-device-detector": "^5.0.1",
"pocketbase": "^0.11.0", "pocketbase": "^0.11.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"sweetalert2": "^11.7.2",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"zone.js": "~0.12.0" "zone.js": "~0.12.0"
}, },

View File

@@ -1,4 +1,5 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import {SeoService} from "./services/seo.service";
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@@ -7,4 +8,6 @@ import { Component } from '@angular/core';
}) })
export class AppComponent { export class AppComponent {
public constructor(private seo: SeoService) {}
} }

View File

@@ -22,6 +22,11 @@ import { ContactComponent } from './sites/contact/contact.component';
import { AboutComponent } from './sites/about/about.component'; import { AboutComponent } from './sites/about/about.component';
import {MatInputModule} from "@angular/material/input"; import {MatInputModule} from "@angular/material/input";
import {ReactiveFormsModule} from "@angular/forms"; import {ReactiveFormsModule} from "@angular/forms";
import {MatSnackBarModule} from "@angular/material/snack-bar";
import { TimestampComponent } from './components/timestamp/timestamp.component';
import { CarrierPipe } from './pipes/carrier.pipe';
import { ExperiencePipe } from './pipes/experience.pipe';
import { FooterComponent } from './components/footer/footer.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@@ -38,7 +43,11 @@ import {ReactiveFormsModule} from "@angular/forms";
FrameworksPipe, FrameworksPipe,
SkillsPipe, SkillsPipe,
ContactComponent, ContactComponent,
AboutComponent AboutComponent,
TimestampComponent,
CarrierPipe,
ExperiencePipe,
FooterComponent
], ],
imports: [ imports: [
BrowserModule.withServerTransition({appId: 'serverApp'}), BrowserModule.withServerTransition({appId: 'serverApp'}),
@@ -48,7 +57,8 @@ import {ReactiveFormsModule} from "@angular/forms";
MatButtonModule, MatButtonModule,
MatTooltipModule, MatTooltipModule,
MatInputModule, MatInputModule,
ReactiveFormsModule ReactiveFormsModule,
MatSnackBarModule
], ],
providers: [], providers: [],
bootstrap: [AppComponent] bootstrap: [AppComponent]

View File

@@ -1,4 +1,14 @@
$text-move: hover-text-move 300ms forwards ease-out; @keyframes hover-text {
from {
opacity: var(--opa-1);
transform: translateY(0);
}
to {
opacity: var(--opa-2);
transform: translateY(-20px);
}
}
.button { .button {
display: flex; display: flex;
@@ -13,42 +23,27 @@ $text-move: hover-text-move 300ms forwards ease-out;
border-radius: 20px; border-radius: 20px;
text-decoration: none; text-decoration: none;
font-size: 13px; font-size: 13px;
overflow: hidden;
.text-1 { .text-1 {
--opa-2: 0;
--opa-1: 1;
margin-top: 12px; margin-top: 12px;
} }
.text-2 { .text-2 {
--opa-2: 1;
--opa-1: 0;
opacity: 0; opacity: 0;
} }
&:hover { &:hover {
.text-1 { .text-1 {
animation: hover-text 250ms forwards ease-out reverse, $text-move; animation: hover-text 300ms forwards ease-out;
} }
.text-2 { .text-2 {
animation: hover-text 300ms forwards ease-out, $text-move; animation: hover-text 300ms forwards ease-out;
} }
} }
} }
@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,11 @@
<footer id="footer" class="home-section">
<span class="footer-title">Portfolio von Leon Hoppe</span>
<a href="mailto://leon@ladenbau-hoppe.de">leon@ladenbau-hoppe.de</a>
<span>+49 1575 8839776</span>
<div id="social-media">
<a class="header-social" target="_blank" *ngFor="let social of socialLinks" href="{{social.href}}">
<img src="{{social.image}}" alt="{{social.href}}" draggable="false">
</a>
</div>
</footer>

View File

@@ -0,0 +1,39 @@
@use "src/theme";
#footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
margin-top: 120px;
user-select: unset;
.footer-title {
background: theme.$gradient;
background-clip: text;
color: transparent;
font-weight: bold;
user-select: none;
}
a {
text-decoration: none;
}
#social-media {
display: flex;
gap: 5px;
user-select: none;
.header-social > img {
width: 25px;
height: 25px;
}
}
}
@media screen and (max-width: theme.$mobile-width) {
#footer *:not(.footer-title) {
display: none;
}
}

View File

@@ -0,0 +1,20 @@
import {Component, OnInit} from '@angular/core';
import {BackendService} from "../../services/backend.service";
import {Social} from "../../models/social";
@Component({
selector: 'app-footer',
templateUrl: './footer.component.html',
styleUrls: ['./footer.component.scss']
})
export class FooterComponent implements OnInit {
public socialLinks: Social[];
public constructor(private backend: BackendService) {}
async ngOnInit() {
this.socialLinks = await this.backend.getSocials();
}
}

View File

@@ -2,22 +2,22 @@
<img src="../../../favicon.ico" alt="logo" class="logo" draggable="false"> <img src="../../../favicon.ico" alt="logo" class="logo" draggable="false">
<span class="name">Leon Hoppe</span> <span class="name">Leon Hoppe</span>
<div id="header-links" *ngIf="!deviceService.isMobile()"> <div id="header-links">
<a class="header-link" *ngFor="let link of navLinks" [routerLink]="link.href" [ngClass]="{'active': cleanUrl(router.url) == link.href}">{{link.label}}</a> <a class="header-link" *ngFor="let link of navLinks" [routerLink]="link.href" [ngClass]="{'active': cleanUrl(router.url) == link.href}">{{link.label}}</a>
</div> </div>
<div id="social-media" [ngStyle]="{'margin-left': deviceService.isMobile() ? 'auto' : ''}"> <div id="social-media">
<a class="header-social" target="_blank" *ngFor="let social of socialLinks" href="{{social.href}}"> <a class="header-social" target="_blank" *ngFor="let social of socialLinks" href="{{social.href}}">
<img src="{{social.image}}" alt="{{social.href}}" draggable="false"> <img src="{{social.image}}" alt="{{social.href}}" draggable="false">
</a> </a>
</div> </div>
</nav> </nav>
<section id="content" [ngStyle]="{'height': deviceService.isMobile() ? 'calc(100% - 102px)' : 'calc(100% - 51px)'}"> <section id="content">
<ng-content></ng-content> <ng-content></ng-content>
</section> </section>
<nav *ngIf="deviceService.isMobile()" class="footer"> <nav class="footer">
<button mat-button class="footer-link" *ngFor="let link of navLinks" [routerLink]="link.href" [ngClass]="{'active': cleanUrl(router.url) == link.href}"> <button mat-button class="footer-link" *ngFor="let link of navLinks" [routerLink]="link.href" [ngClass]="{'active': cleanUrl(router.url) == link.href}">
<mat-icon>{{link.icon}}</mat-icon> <mat-icon>{{link.icon}}</mat-icon>
</button> </button>

View File

@@ -70,8 +70,8 @@
width: 100%; width: 100%;
border-top: 1px solid theme.$border-color; border-top: 1px solid theme.$border-color;
background-color: map.get(theme.$background, 'background'); background-color: map.get(theme.$background, 'background');
display: none;
display: grid;
grid-auto-columns: minmax(0, 1fr); grid-auto-columns: minmax(0, 1fr);
grid-auto-flow: column; grid-auto-flow: column;
@@ -106,4 +106,25 @@
#content { #content {
overflow: auto; overflow: auto;
height: calc(100% - 51px);
}
@media screen and (max-width: theme.$mobile-width) {
.header {
#header-links {
display: none;
}
#social-media {
margin-left: auto;
}
}
#content {
height: calc(100% - 102px);
}
.footer {
display: grid;
}
} }

View File

@@ -1,7 +1,7 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {DeviceDetectorService} from "ngx-device-detector";
import {Router} from "@angular/router"; import {Router} from "@angular/router";
import {BackendService} from "../../services/backend.service"; import {BackendService} from "../../services/backend.service";
import {Social} from "../../models/social";
@Component({ @Component({
selector: 'app-navigation', selector: 'app-navigation',
@@ -10,7 +10,7 @@ import {BackendService} from "../../services/backend.service";
}) })
export class NavigationComponent implements OnInit { export class NavigationComponent implements OnInit {
public constructor(public deviceService: DeviceDetectorService, public router: Router, private backend: BackendService) {} public constructor(public router: Router, private backend: BackendService) {}
public navLinks: {label: string, href: string, icon?: string}[] = [ public navLinks: {label: string, href: string, icon?: string}[] = [
{label: 'Home', href: '/', icon: 'home'}, {label: 'Home', href: '/', icon: 'home'},
@@ -20,7 +20,7 @@ export class NavigationComponent implements OnInit {
{label: 'Kontakt', href: '/contact', icon: 'mail'} {label: 'Kontakt', href: '/contact', icon: 'mail'}
]; ];
public socialLinks: {href: string, image: string}[]; public socialLinks: Social[];
public cleanUrl(url: string): string { public cleanUrl(url: string): string {
try { try {

View File

@@ -0,0 +1,4 @@
<div class="timestamp" #timestampRef>
<h2>{{timestamp.date}}</h2>
<span>{{timestamp.description}}</span>
</div>

View File

@@ -0,0 +1,73 @@
@use "src/theme";
.timestamp {
flex-grow: 1;
flex-basis: 0;
display: flex;
flex-direction: column;
gap: 50px;
position: relative;
opacity: 0;
h2 {
font-size: 20px;
margin: 0;
font-weight: normal;
}
span {
box-sizing: border-box;
color: theme.$desc-color;
font-size: 14px;
padding-right: 10px;
}
&:after {
content: '';
width: 15px;
height: 15px;
background: theme.$gradient-angled;
border-radius: 50%;
box-shadow: 0 3px 10px 0.5px rgba(theme.$primary, 0.4);
position: absolute;
top: 45px;
}
&:before {
content: '';
width: 0;
height: 3px;
background-color: #FFF;
top: 51px;
position: absolute;
display: var(--show-bar, block);
}
}
@media screen and (max-width: theme.$mobile-width) {
.timestamp {
gap: 15px;
padding-left: 30px;
box-sizing: border-box;
animation: none !important;
opacity: 1;
span {
margin-bottom: 50px;
}
&:after {
top: 5px;
left: -5px;
}
&:before {
top: 5px;
left: 1px;
width: 3px;
height: 100%;
animation: none !important;
}
}
}

View File

@@ -0,0 +1,13 @@
import {Component, Input} from '@angular/core';
import {Timestamp} from "../../models/timestamp";
@Component({
selector: 'app-timestamp',
templateUrl: './timestamp.component.html',
styleUrls: ['./timestamp.component.scss']
})
export class TimestampComponent {
@Input('timestamp') timestamp: Timestamp;
}

4
src/app/models/about.ts Normal file
View File

@@ -0,0 +1,4 @@
export interface About {
about: string;
future: string;
}

View File

@@ -2,4 +2,5 @@ export interface Timestamp {
date: number, date: number,
description: string; description: string;
featured?: boolean; featured?: boolean;
carrier?: boolean;
} }

View File

@@ -0,0 +1,13 @@
import { Pipe, PipeTransform } from '@angular/core';
import {Timestamp} from "../models/timestamp";
@Pipe({
name: 'carrier'
})
export class CarrierPipe implements PipeTransform {
transform(objects: Timestamp[]): Timestamp[] {
return objects?.filter(obj => obj.carrier);
}
}

View File

@@ -0,0 +1,13 @@
import { Pipe, PipeTransform } from '@angular/core';
import {Timestamp} from "../models/timestamp";
@Pipe({
name: 'experience'
})
export class ExperiencePipe implements PipeTransform {
transform(objects: Timestamp[]): Timestamp[] {
return objects?.filter(obj => !obj.carrier);
}
}

View File

@@ -16,4 +16,8 @@ export class AnimatorService {
}); });
} }
public getAnimationDelay(index: number, multiplier = 150, additional = 0): string {
return `${index * multiplier + additional}ms`;
}
} }

View File

@@ -5,6 +5,7 @@ import {Technology} from "../models/technology";
import {Timestamp} from "../models/timestamp"; import {Timestamp} from "../models/timestamp";
import {Social} from "../models/social"; import {Social} from "../models/social";
import {Message} from "../models/message"; import {Message} from "../models/message";
import {About} from "../models/about";
@Injectable({ @Injectable({
@@ -14,6 +15,13 @@ export class BackendService {
private pb: PocketBase; private pb: PocketBase;
private states: {id: string, name: string}[] = [
{id: 'finished', name: "Fertig"},
{id: 'canceled', name: "Abgebrochen"},
{id: 'paused', name: "Pausiert"},
{id: 'development', name: "In Entwicklung"}
]
constructor() { constructor() {
this.pb = new PocketBase('https://ed168214-77da-44f1-9a61-859abb49edf8.api.leon-hoppe.de'); this.pb = new PocketBase('https://ed168214-77da-44f1-9a61-859abb49edf8.api.leon-hoppe.de');
} }
@@ -23,13 +31,12 @@ export class BackendService {
sort: '-order' sort: '-order'
}) as Project[]; }) as Project[];
const allLanguages = await this.pb?.collection('languages').getFullList(); const allLanguages = await this.pb?.collection('languages').getFullList();
const states = await this.pb?.collection('project_states').getFullList();
const projects: Project[] = []; const projects: Project[] = [];
for(let rawProject of rawProjects) { for(let rawProject of rawProjects) {
const project = rawProject as Project; const project = rawProject as Project;
project.status = states?.filter(state => state.id == rawProject.status)[0]['name']; project.status = this.states?.filter(state => state.id == rawProject.status)[0]['name'];
if (rawProject.languages != undefined) { if (rawProject.languages != undefined) {
const languages: Language[] = [] const languages: Language[] = []
@@ -50,7 +57,9 @@ export class BackendService {
} }
public async getTimeline(): Promise<Timestamp[]> { public async getTimeline(): Promise<Timestamp[]> {
return await this.pb?.collection('timeline').getFullList(); return await this.pb?.collection('timeline').getFullList(200, {
sort: 'date'
});
} }
public async getSocials(): Promise<Social[]> { public async getSocials(): Promise<Social[]> {
@@ -61,6 +70,10 @@ export class BackendService {
]; ];
} }
public async getAbout(): Promise<About> {
return await this.pb?.collection('about').getFirstListItem('');
}
public async sendMessage(message: Message) { public async sendMessage(message: Message) {
await this.pb?.collection('messages').create(message); await this.pb?.collection('messages').create(message);
} }

View File

@@ -0,0 +1,32 @@
import { Injectable } from '@angular/core';
import {Meta, Title} from "@angular/platform-browser";
@Injectable({
providedIn: 'root'
})
export class SeoService {
constructor(private title: Title, private meta: Meta) {
this.setDefaults();
}
public setTitle(title: string): void {
this.title.setTitle(title);
this.meta.updateTag({property: "og:title", content: title});
}
public setDescription(description: string): void {
this.meta.updateTag({property: "description", content: description});
this.meta.updateTag({property: "og:description", content: description});
}
public setDefaults(): void {
this.meta.updateTag({property: "description", content: "Auf dieser Seite erfahren Sie, an welchen Projekten ich bereits gearbeitet habe, was meine Programmierkenntnisse sind und welche Pläne ich für die Zukunft habe."});
this.meta.updateTag({property: "og:description", content: "Auf dieser Seite erfahren Sie, an welchen Projekten ich bereits gearbeitet habe, was meine Programmierkenntnisse sind und welche Pläne ich für die Zukunft habe."});
this.meta.updateTag({property: "og:url", content: "https://leon-hoppe.de/"});
this.meta.updateTag({property: "og:title", content: "Portfolio von Leon Hoppe"});
this.meta.updateTag({property: "og:image", content: "https://leon-hoppe.de/favicon.ico"});
this.title.setTitle("Portfolio von Leon Hoppe");
}
}

View File

@@ -1 +1,26 @@
<p>about works!</p> <section class="home-section" id="about">
<div>
<h1 class="title">Über mich</h1>
<p [innerText]="about?.about"></p>
</div>
<div>
<h1 class="title">Zukünftige Projekte</h1>
<p [innerText]="about?.future"></p>
</div>
</section>
<section class="home-section" #experience>
<h1 class="title">Programmiererfahrung</h1>
<div class="timeline">
<app-timestamp *ngFor="let timestamp of timeline | experience; let i = index" [timestamp]="timestamp" [ngStyle]="{'--delay': animator.getAnimationDelay(i, 500)}" />
</div>
</section>
<section class="home-section" #carrier>
<h1 class="title">Karriere</h1>
<div class="timeline">
<app-timestamp *ngFor="let timestamp of timeline | carrier; let i = index" [timestamp]="timestamp" [ngStyle]="{'--delay': animator.getAnimationDelay(i, 500)}" />
</div>
</section>
<app-footer />

View File

@@ -0,0 +1,13 @@
#about {
margin-top: 50px;
display: grid;
gap: 20px;
grid-template-columns: repeat(2, 1fr);
}
@media screen and (max-width: 1200px) {
#about {
grid-template-columns: unset;
grid-template-rows: repeat(2, max-content);
}
}

View File

@@ -1,10 +1,35 @@
import { Component } from '@angular/core'; import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core';
import {Timestamp} from "../../models/timestamp";
import {BackendService} from "../../services/backend.service";
import {AnimatorService} from "../../services/animator.service";
import {About} from "../../models/about";
import {SeoService} from "../../services/seo.service";
@Component({ @Component({
selector: 'app-about', selector: 'app-about',
templateUrl: './about.component.html', templateUrl: './about.component.html',
styleUrls: ['./about.component.scss'] styleUrls: ['./about.component.scss']
}) })
export class AboutComponent { export class AboutComponent implements OnInit, AfterViewInit {
@ViewChild('experience') experience: ElementRef;
@ViewChild('carrier') carrier: ElementRef;
public about: About;
public timeline: Timestamp[];
public constructor(private backend: BackendService, public animator: AnimatorService, public seo: SeoService) {
this.seo.setDefaults();
this.seo.setTitle("Über mich");
}
async ngOnInit() {
this.about = await this.backend.getAbout();
this.timeline = await this.backend.getTimeline();
}
ngAfterViewInit(): void {
this.animator.observer.observe(this.experience.nativeElement);
this.animator.observer.observe(this.carrier.nativeElement);
}
} }

View File

@@ -1,5 +1,6 @@
<div class="hider" [ngClass]="{'mobile': device.isMobile()}"> <div class="hider">
<div class="form-wrapper" [ngClass]="{'mobile': device.isMobile()}">
<div class="form-wrapper">
<form [formGroup]="form" (ngSubmit)="sendMessage()"> <form [formGroup]="form" (ngSubmit)="sendMessage()">
<section id="contact-info"> <section id="contact-info">
<h1>Kontakt</h1> <h1>Kontakt</h1>

View File

@@ -3,10 +3,6 @@
.hider { .hider {
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;
&.mobile {
overflow-y: auto;
}
} }
.form-wrapper { .form-wrapper {
@@ -64,8 +60,14 @@
} }
} }
} }
}
&.mobile { @media screen and (max-width: theme.$mobile-width) {
.hider {
overflow-y: auto;
}
.form-wrapper {
display: block; display: block;
height: max-content; height: max-content;

View File

@@ -1,8 +1,8 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import {FormControl, FormGroup, Validators} from "@angular/forms"; import {FormControl, FormGroup, Validators} from "@angular/forms";
import Swal from 'sweetalert2/dist/sweetalert2.js';
import {BackendService} from "../../services/backend.service"; import {BackendService} from "../../services/backend.service";
import {DeviceDetectorService} from "ngx-device-detector"; import {MatSnackBar} from "@angular/material/snack-bar";
import {SeoService} from "../../services/seo.service";
@Component({ @Component({
selector: 'app-contact', selector: 'app-contact',
@@ -17,7 +17,10 @@ export class ContactComponent {
message: new FormControl('', [Validators.required]) message: new FormControl('', [Validators.required])
}); });
public constructor(public backend: BackendService, public device: DeviceDetectorService) {} public constructor(public backend: BackendService, private snackbar: MatSnackBar, private seo: SeoService) {
seo.setTitle("Kontakt");
seo.setDescription("Schreiben Sie mir eine Nachricht");
}
public async sendMessage() { public async sendMessage() {
if (!this.form.valid) return; if (!this.form.valid) return;
@@ -29,12 +32,7 @@ export class ContactComponent {
}); });
this.form.reset(); this.form.reset();
Swal.fire({ this.snackbar.open("Nachricht gesendet!", undefined, {duration: 2000});
icon: 'success',
title: 'Nachricht gesendet',
showConfirmButton: false,
timer: 1500
});
} }
} }

View File

@@ -1,12 +1,12 @@
<section id="hero" [ngClass]="{'mobile': deviceService.isMobile()}"> <section id="hero">
<div class="artwork"> <div class="artwork">
<div class="circle big-circle"></div> <div class="circle big-circle"></div>
<div class="circle small-circle"></div> <div class="circle small-circle"></div>
<div class="circle image"></div> <div class="circle image"></div>
</div> </div>
<h1> <h1>
<span>Hallo, ich bin Leon Hoppe,</span><br> <span id="welcome">Hallo, ich bin Leon Hoppe,</span><br>
full stack developer <span id="jobs">{{jobs.display}}</span>
</h1> </h1>
<p> <p>
Auf dieser Seite erfahren Sie, an welchen Projekten ich bereits gearbeitet habe,<br> Auf dieser Seite erfahren Sie, an welchen Projekten ich bereits gearbeitet habe,<br>
@@ -15,15 +15,15 @@
<a href="#projects">Mehr erfahren</a> <a href="#projects">Mehr erfahren</a>
</section> </section>
<section id="projects" class="home-section" [ngClass]="{'mobile': deviceService.isMobile()}"> <section id="projects" class="home-section">
<h1 class="title">Projekte</h1> <h1 class="title">Projekte</h1>
<a routerLink="/projects">alle ansehen</a> <a routerLink="/projects">alle ansehen</a>
<div id="projects-wrapper" #projectsWrapper> <div id="projects-wrapper" #projectsWrapper>
<app-project *ngFor="let project of projects | featured; let i = index" [project]="project" [ngStyle]="{'animation-delay': getAnimationDelay(i)}" /> <app-project *ngFor="let project of projects | featured; let i = index" [project]="project" [ngStyle]="{'animation-delay': animator.getAnimationDelay(i)}" />
</div> </div>
</section> </section>
<section id="technologies" class="home-section" [ngClass]="{'mobile': deviceService.isMobile()}"> <section id="technologies" class="home-section">
<h1 class="title">Technologien</h1> <h1 class="title">Technologien</h1>
<a routerLink="/technologies">mehr erfahren</a> <a routerLink="/technologies">mehr erfahren</a>
<div class="technologies-wrapper"> <div class="technologies-wrapper">
@@ -31,25 +31,12 @@
</div> </div>
</section> </section>
<section id="about" class="home-section" [ngClass]="{'mobile': deviceService.isMobile()}"> <section id="about" class="home-section">
<h1 class="title">Über mich</h1> <h1 class="title">Über mich</h1>
<a routerLink="/about">mehr erfahren</a> <a routerLink="/about">mehr erfahren</a>
<div id="timeline" #timelineElement> <div class="timeline" #timelineElement>
<div class="timestamp" *ngFor="let timestamp of timeline | featured; let i = index" [ngStyle]="{'--delay': getAnimationDelay(i, 500)}"> <app-timestamp *ngFor="let timestamp of timeline | featured; let i = index" [timestamp]="timestamp" [ngStyle]="{'--delay': animator.getAnimationDelay(i, 500)}" />
<h2>{{timestamp.date}}</h2>
<span>{{timestamp.description}}</span>
</div>
</div> </div>
</section> </section>
<section id="footer" class="home-section" [ngClass]="{'mobile': deviceService.isMobile()}"> <app-footer />
<span class="footer-title">Portfolio von Leon Hoppe</span>
<a href="mailto://leon@ladenbau-hoppe.de">leon@ladenbau-hoppe.de</a>
<span>+49 1575 8839776</span>
<div id="social-media">
<a class="header-social" target="_blank" *ngFor="let social of socialLinks" href="{{social.href}}">
<img src="{{social.image}}" alt="{{social.href}}" draggable="false">
</a>
</div>
</section>

View File

@@ -15,11 +15,26 @@
font-size: 45px; font-size: 45px;
line-height:70px; line-height:70px;
span { #welcome {
background: theme.$gradient; background: theme.$gradient;
background-clip: text; background-clip: text;
color: transparent; color: transparent;
} }
#jobs {
position: relative;
&:after {
content: '';
position: absolute;
left: calc(100% + 5px);
top: 0;
width: 20px;
height: 100%;
background-color: map.get(theme.$text, 'text');
animation: blink 800ms infinite;
}
}
} }
p { p {
@@ -41,25 +56,6 @@
box-shadow: 0 0 40px -5px theme.$primary; box-shadow: 0 0 40px -5px theme.$primary;
} }
&.mobile {
padding-left: theme.$padding-small;
h1 {
margin-top: 10vh;
font-size: 30px;
line-height: 50px;
}
p {
font-size: 15px;
margin-right: 20px;
}
.artwork > .small-circle, .artwork > .image {
display: none;
}
}
} }
#projects { #projects {
@@ -79,13 +75,6 @@
animation: fade-in 250ms forwards; animation: fade-in 250ms forwards;
} }
} }
&.mobile {
#projects-wrapper {
margin-top: 30px;
gap: 30px;
}
}
} }
#technologies { #technologies {
@@ -94,152 +83,44 @@
#about { #about {
margin-top: 150px; margin-top: 150px;
#timeline {
display: flex;
margin-top: 30px;
.timestamp {
flex-grow: 1;
flex-basis: 0;
display: flex;
flex-direction: column;
gap: 50px;
position: relative;
opacity: 0;
h2 {
font-size: 20px;
margin: 0;
font-weight: normal;
}
span {
box-sizing: border-box;
color: theme.$desc-color;
font-size: 14px;
padding-right: 10px;
}
&:after {
content: '';
width: 15px;
height: 15px;
background: theme.$gradient-angled;
border-radius: 50%;
box-shadow: 0 3px 10px 0.5px rgba(theme.$primary, 0.4);
position: absolute;
top: 45px;
}
&:before {
content: '';
width: 0;
height: 3px;
background-color: #FFF;
top: 51px;
position: absolute;
}
&:last-of-type:before {
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 {
#timeline {
flex-direction: column-reverse;
.timestamp {
gap: 15px;
padding-left: 30px;
box-sizing: border-box;
animation: none;
opacity: 1;
span {
margin-bottom: 50px;
}
&:after {
top: 5px;
left: -5px;
}
&:before {
top: 5px;
left: 1px;
width: 3px;
height: 100%;
animation: none;
}
&:last-of-type:before {
display: block;
}
&:first-of-type:before {
display: none;
}
}
}
}
} }
#footer { @media screen and (max-width: theme.$mobile-width) {
display: flex; #hero {
justify-content: space-between; padding-left: theme.$padding-small;
align-items: center;
margin-bottom: 30px;
margin-top: 120px;
user-select: unset;
.footer-title { h1 {
background: theme.$gradient; margin-top: 10vh;
background-clip: text; font-size: 30px;
color: transparent; line-height: 50px;
font-weight: bold;
user-select: none;
}
a {
text-decoration: none;
}
#social-media {
display: flex;
gap: 5px;
user-select: none;
.header-social > img {
width: 25px;
height: 25px;
} }
}
&.mobile { p {
*:not(.footer-title) { font-size: 15px;
margin-right: 20px;
}
.artwork > .small-circle, .artwork > .image {
display: none; display: none;
} }
} }
}
@keyframes timestamp-in { #projects #projects-wrapper {
from { margin-top: 30px;
width: 0; gap: 30px;
} }
}
to {
width: 100%; @keyframes blink {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
} }
} }

View File

@@ -1,10 +1,10 @@
import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core'; import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core';
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 {BackendService} from "../../services/backend.service";
import {AnimatorService} from "../../services/animator.service"; import {AnimatorService} from "../../services/animator.service";
import {Timestamp} from "../../models/timestamp"; import {Timestamp} from "../../models/timestamp";
import {SeoService} from "../../services/seo.service";
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
@@ -20,10 +20,16 @@ export class HomeComponent implements OnInit, AfterViewInit {
public timeline: Timestamp[]; public timeline: Timestamp[];
public socialLinks: {href: string, image: string}[]; public socialLinks: {href: string, image: string}[];
public constructor(public deviceService: DeviceDetectorService, private backend: BackendService, private animator: AnimatorService) {} public jobs: {current: number, all: string[], state: number, display: string} = {
current: 0,
all: ["full stack developer", "C# developer", "Java developer"],
state: 0,
display: ""
};
public getAnimationDelay(index: number, multiplier = 150): string { public constructor(private backend: BackendService, public animator: AnimatorService, private seo: SeoService) {
return `${index * multiplier}ms`; setInterval(this.handleJobsAnimation.bind(this), 50);
seo.setDefaults();
} }
async ngOnInit() { async ngOnInit() {
@@ -38,4 +44,23 @@ export class HomeComponent implements OnInit, AfterViewInit {
this.animator.observer.observe(this.timelineElement.nativeElement); this.animator.observer.observe(this.timelineElement.nativeElement);
} }
private handleJobsAnimation(): void {
if (this.jobs.state == 0) {
const len = this.jobs.display.length;
this.jobs.display = this.jobs.all[this.jobs.current].slice(0, len + 1);
if (this.jobs.display.length >= this.jobs.all[this.jobs.current].length) this.jobs.state = 1;
} else if (this.jobs.state == 50) {
const len = this.jobs.display.length;
this.jobs.display = this.jobs.display.slice(0, len - 1);
if (this.jobs.display.length <= 1) {
this.jobs.state = 0;
this.jobs.current = (this.jobs.current + 1) % this.jobs.all.length;
}
} else {
this.jobs.state++;
}
}
} }

View File

@@ -1,7 +1,7 @@
<section class="home-section" [ngClass]="{'mobile': deviceService.isMobile()}"> <section class="home-section">
<h1 class="title">Alle Projekte</h1> <h1 class="title">Alle Projekte</h1>
<div class="projects-wrapper"> <div class="projects-wrapper">
<div class="project-wrapper" *ngFor="let project of projects; let i = index" [ngStyle]="{'animation-delay': getAnimationDelay(i)}"> <div class="project-wrapper" *ngFor="let project of projects; let i = index" [ngStyle]="{'animation-delay': animator.getAnimationDelay(i)}">
<app-project [project]="project" showInfo /> <app-project [project]="project" showInfo />
</div> </div>
</div> </div>

View File

@@ -1,7 +1,8 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {Project} from "../../models/project"; import {Project} from "../../models/project";
import {DeviceDetectorService} from "ngx-device-detector";
import {BackendService} from "../../services/backend.service"; import {BackendService} from "../../services/backend.service";
import {AnimatorService} from "../../services/animator.service";
import {SeoService} from "../../services/seo.service";
@Component({ @Component({
selector: 'app-projects', selector: 'app-projects',
@@ -10,14 +11,13 @@ import {BackendService} from "../../services/backend.service";
}) })
export class ProjectsComponent implements OnInit { export class ProjectsComponent implements OnInit {
public constructor(public deviceService: DeviceDetectorService, private backend: BackendService) {} public constructor(private backend: BackendService, public animator: AnimatorService, private seo: SeoService) {
seo.setTitle("Projekte");
seo.setDescription("Ein Überblick von all meinen Projekten");
}
public projects: Project[] | undefined; public projects: Project[] | undefined;
public getAnimationDelay(index: number): string {
return `${index * 150}ms`;
}
async ngOnInit() { async ngOnInit() {
this.projects = await this.backend.getProjects(); this.projects = await this.backend.getProjects();
} }

View File

@@ -1,4 +1,4 @@
<section class="home-section" id="tech-projects" [ngClass]="{'mobile': deviceService.isMobile()}"> <section class="home-section" id="tech-projects">
<div class="artwork"> <div class="artwork">
<div class="circle big-circle"></div> <div class="circle big-circle"></div>
<div class="circle small-circle"></div> <div class="circle small-circle"></div>
@@ -10,21 +10,21 @@
</div> </div>
</section> </section>
<section class="home-section" id="languages" [ngClass]="{'mobile': deviceService.isMobile()}"> <section class="home-section" id="languages">
<h1 class="title">Programmiersprachen</h1> <h1 class="title">Programmiersprachen</h1>
<div class="technologies-wrapper"> <div class="technologies-wrapper">
<app-technology *ngFor="let technology of technologies | languages" [technology]="technology" /> <app-technology *ngFor="let technology of technologies | languages" [technology]="technology" />
</div> </div>
</section> </section>
<section class="home-section" id="frameworks" [ngClass]="{'mobile': deviceService.isMobile()}"> <section class="home-section" id="frameworks">
<h1 class="title">Frameworks</h1> <h1 class="title">Frameworks</h1>
<div class="technologies-wrapper"> <div class="technologies-wrapper">
<app-technology *ngFor="let technology of technologies | frameworks" [technology]="technology" /> <app-technology *ngFor="let technology of technologies | frameworks" [technology]="technology" />
</div> </div>
</section> </section>
<section class="home-section" id="additional" [ngClass]="{'mobile': deviceService.isMobile()}"> <section class="home-section" id="additional">
<h1 class="title">Zusätzliche Fähigkeiten</h1> <h1 class="title">Zusätzliche Fähigkeiten</h1>
<div id="skills-wrapper"> <div id="skills-wrapper">
<div class="skill" *ngFor="let skill of technologies | skills"> <div class="skill" *ngFor="let skill of technologies | skills">

View File

@@ -2,7 +2,7 @@
@use "sass:map"; @use "sass:map";
#tech-projects { #tech-projects {
margin-top: 200px; margin-top: 50px;
position: relative; position: relative;
.chart { .chart {
@@ -28,16 +28,6 @@
justify-content: center; justify-content: center;
} }
} }
&.mobile {
margin-top: 50px;
.chart .chart-container {
aspect-ratio: 1 / 1;
width: 100%;
height: auto;
}
}
} }
#additional { #additional {
@@ -68,8 +58,20 @@
} }
} }
} }
}
&.mobile #skills-wrapper { @media screen and (max-width: theme.$mobile-width) {
#additional #skills-wrapper {
flex-direction: column; flex-direction: column;
} }
#tech-projects {
margin-top: 50px;
.chart .chart-container {
aspect-ratio: 1 / 1;
width: 100%;
height: auto;
}
}
} }

View File

@@ -2,7 +2,7 @@ import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core';
import Chart from 'chart.js/auto'; import Chart from 'chart.js/auto';
import {BackendService} from "../../services/backend.service"; import {BackendService} from "../../services/backend.service";
import {Technology} from "../../models/technology"; import {Technology} from "../../models/technology";
import {DeviceDetectorService} from "ngx-device-detector"; import {SeoService} from "../../services/seo.service";
@Component({ @Component({
selector: 'app-technologies', selector: 'app-technologies',
@@ -14,7 +14,10 @@ export class TechnologiesComponent implements AfterViewInit {
@ViewChild('chart') chartRef: ElementRef; @ViewChild('chart') chartRef: ElementRef;
public technologies: Technology[]; public technologies: Technology[];
public constructor(public deviceService: DeviceDetectorService, private backend: BackendService) {} public constructor(private backend: BackendService, private seo: SeoService) {
seo.setTitle("Technologien");
seo.setDescription("Eine Übersicht über alle Technologien, die ich Benutze");
}
async ngAfterViewInit() { async ngAfterViewInit() {
const projects = await this.backend.getProjects(); const projects = await this.backend.getProjects();

View File

@@ -4,6 +4,13 @@
<meta charset="utf-8"> <meta charset="utf-8">
<title>Portfolio von Leon Hoppe</title> <title>Portfolio von Leon Hoppe</title>
<base href="/"> <base href="/">
<meta name="description" content="Auf dieser Seite erfahren Sie, an welchen Projekten ich bereits gearbeitet habe, was meine Programmierkenntnisse sind und welche Pläne ich für die Zukunft habe.">
<meta property="og:url" content="https://leon-hoppe.de/">
<meta property="og:title" content="Portfolio von Leon Hoppe">
<meta property="og:description" content="Auf dieser Seite erfahren Sie, an welchen Projekten ich bereits gearbeitet habe, was meine Programmierkenntnisse sind und welche Pläne ich für die Zukunft habe.">
<meta property="og:image" content="https://leon-hoppe.de/favicon.ico">
<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">

4
src/robots.txt Normal file
View File

@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://leon-hoppe.de/sitemap.xml

23
src/sitemap.xml Normal file
View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://leon-hoppe.de/</loc>
<lastmod>2023-02-24</lastmod>
</url>
<url>
<loc>https://leon-hoppe.de/projects</loc>
<lastmod>2023-02-24</lastmod>
</url>
<url>
<loc>https://leon-hoppe.de/technologies</loc>
<lastmod>2023-02-24</lastmod>
</url>
<url>
<loc>https://leon-hoppe.de/about</loc>
<lastmod>2023-02-24</lastmod>
</url>
<url>
<loc>https://leon-hoppe.de/contact</loc>
<lastmod>2023-02-24</lastmod>
</url>
</urlset>

View File

@@ -1,8 +1,6 @@
@use "sass:map"; @use "sass:map";
@use "theme"; @use "theme";
@import '@sweetalert2/theme-dark/dark.scss';
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; }
*, html {scroll-behavior: smooth !important;} *, html {scroll-behavior: smooth !important;}
@@ -104,6 +102,63 @@ mat-drawer > div {
} }
} }
.title {
font-size: 35px;
display: inline;
margin-right: 10px;
}
.home-section {
padding-inline: theme.$padding;
user-select: none;
margin-bottom: 100px;
}
.timeline {
display: flex;
margin-top: 30px;
app-timestamp {
flex: 1 1 0;
&:last-of-type {
--show-bar: none;
}
}
}
.in-view {
.timestamp {
animation: fade-in 200ms forwards var(--delay) ease-out;
&:before {
animation: timestamp-in 500ms forwards var(--delay) ease-in-out;
}
}
}
@media screen and (max-width: theme.$mobile-width) {
.home-section {
padding-inline: theme.$padding-small;
.title {
font-size: 25px;
}
}
.timeline {
flex-direction: column-reverse;
app-timestamp:last-of-type {
--show-bar: block;
}
app-timestamp:first-of-type {
--show-bar: none;
}
}
}
@keyframes fade-in { @keyframes fade-in {
from { from {
opacity: 0; opacity: 0;
@@ -113,3 +168,13 @@ mat-drawer > div {
opacity: 1; opacity: 1;
} }
} }
@keyframes timestamp-in {
from {
width: 0;
}
to {
width: 100%;
}
}

View File

@@ -50,6 +50,7 @@ $gradient-straight: linear-gradient($primary, $secondary);
$padding: 12.5vw; $padding: 12.5vw;
$padding-small: 5vw; $padding-small: 5vw;
$mobile-width: 750px;
$desc-color: #7c8393; $desc-color: #7c8393;
$border-color: #2d2d2d; $border-color: #2d2d2d;
@@ -66,26 +67,6 @@ 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;
}
}
}
.mat-mdc-text-field-wrapper { .mat-mdc-text-field-wrapper {
background-color: map.get($background, 'background') !important; background-color: map.get($background, 'background') !important;
} }