finished v1.0
This commit is contained in:
14
.dockerignore
Normal file
14
.dockerignore
Normal 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
11
Dockerfile
Normal 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 |
12
angular.json
12
angular.json
@@ -27,7 +27,9 @@
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
"src/assets",
|
||||
"src/robots.txt",
|
||||
"src/sitemap.xml"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss",
|
||||
@@ -40,13 +42,13 @@
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
"maximumWarning": "5mb",
|
||||
"maximumError": "10mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
"maximumWarning": "100kb",
|
||||
"maximumError": "100kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
|
||||
47
package-lock.json
generated
47
package-lock.json
generated
@@ -20,13 +20,10 @@
|
||||
"@angular/platform-server": "^15.1.0",
|
||||
"@angular/router": "^15.1.0",
|
||||
"@nguniversal/express-engine": "^15.1.0",
|
||||
"@sweetalert2/theme-dark": "^5.0.15",
|
||||
"chart.js": "^4.2.1",
|
||||
"express": "^4.15.2",
|
||||
"ngx-device-detector": "^5.0.1",
|
||||
"pocketbase": "^0.11.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"sweetalert2": "^11.7.2",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.12.0"
|
||||
},
|
||||
@@ -3933,11 +3930,6 @@
|
||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==",
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
||||
@@ -10036,18 +10028,6 @@
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz",
|
||||
@@ -12453,15 +12433,6 @@
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
||||
@@ -21312,14 +21278,6 @@
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz",
|
||||
@@ -23136,11 +23094,6 @@
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
|
||||
|
||||
@@ -26,13 +26,10 @@
|
||||
"@angular/platform-server": "^15.1.0",
|
||||
"@angular/router": "^15.1.0",
|
||||
"@nguniversal/express-engine": "^15.1.0",
|
||||
"@sweetalert2/theme-dark": "^5.0.15",
|
||||
"chart.js": "^4.2.1",
|
||||
"express": "^4.15.2",
|
||||
"ngx-device-detector": "^5.0.1",
|
||||
"pocketbase": "^0.11.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"sweetalert2": "^11.7.2",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.12.0"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {SeoService} from "./services/seo.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -7,4 +8,6 @@ import { Component } from '@angular/core';
|
||||
})
|
||||
export class AppComponent {
|
||||
|
||||
public constructor(private seo: SeoService) {}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,11 @@ import { ContactComponent } from './sites/contact/contact.component';
|
||||
import { AboutComponent } from './sites/about/about.component';
|
||||
import {MatInputModule} from "@angular/material/input";
|
||||
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({
|
||||
declarations: [
|
||||
@@ -38,7 +43,11 @@ import {ReactiveFormsModule} from "@angular/forms";
|
||||
FrameworksPipe,
|
||||
SkillsPipe,
|
||||
ContactComponent,
|
||||
AboutComponent
|
||||
AboutComponent,
|
||||
TimestampComponent,
|
||||
CarrierPipe,
|
||||
ExperiencePipe,
|
||||
FooterComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule.withServerTransition({appId: 'serverApp'}),
|
||||
@@ -48,7 +57,8 @@ import {ReactiveFormsModule} from "@angular/forms";
|
||||
MatButtonModule,
|
||||
MatTooltipModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule
|
||||
ReactiveFormsModule,
|
||||
MatSnackBarModule
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
|
||||
@@ -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 {
|
||||
display: flex;
|
||||
@@ -13,42 +23,27 @@ $text-move: hover-text-move 300ms forwards ease-out;
|
||||
border-radius: 20px;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
|
||||
.text-1 {
|
||||
--opa-2: 0;
|
||||
--opa-1: 1;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.text-2 {
|
||||
--opa-2: 1;
|
||||
--opa-1: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.text-1 {
|
||||
animation: hover-text 250ms forwards ease-out reverse, $text-move;
|
||||
animation: hover-text 300ms forwards ease-out;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
11
src/app/components/footer/footer.component.html
Normal file
11
src/app/components/footer/footer.component.html
Normal 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>
|
||||
39
src/app/components/footer/footer.component.scss
Normal file
39
src/app/components/footer/footer.component.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
20
src/app/components/footer/footer.component.ts
Normal file
20
src/app/components/footer/footer.component.ts
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,22 +2,22 @@
|
||||
<img src="../../../favicon.ico" alt="logo" class="logo" draggable="false">
|
||||
<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>
|
||||
</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}}">
|
||||
<img src="{{social.image}}" alt="{{social.href}}" draggable="false">
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<section id="content" [ngStyle]="{'height': deviceService.isMobile() ? 'calc(100% - 102px)' : 'calc(100% - 51px)'}">
|
||||
<section id="content">
|
||||
<ng-content></ng-content>
|
||||
</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}">
|
||||
<mat-icon>{{link.icon}}</mat-icon>
|
||||
</button>
|
||||
|
||||
@@ -70,8 +70,8 @@
|
||||
width: 100%;
|
||||
border-top: 1px solid theme.$border-color;
|
||||
background-color: map.get(theme.$background, 'background');
|
||||
display: none;
|
||||
|
||||
display: grid;
|
||||
grid-auto-columns: minmax(0, 1fr);
|
||||
grid-auto-flow: column;
|
||||
|
||||
@@ -106,4 +106,25 @@
|
||||
|
||||
#content {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {DeviceDetectorService} from "ngx-device-detector";
|
||||
import {Router} from "@angular/router";
|
||||
import {BackendService} from "../../services/backend.service";
|
||||
import {Social} from "../../models/social";
|
||||
|
||||
@Component({
|
||||
selector: 'app-navigation',
|
||||
@@ -10,7 +10,7 @@ import {BackendService} from "../../services/backend.service";
|
||||
})
|
||||
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}[] = [
|
||||
{label: 'Home', href: '/', icon: 'home'},
|
||||
@@ -20,7 +20,7 @@ export class NavigationComponent implements OnInit {
|
||||
{label: 'Kontakt', href: '/contact', icon: 'mail'}
|
||||
];
|
||||
|
||||
public socialLinks: {href: string, image: string}[];
|
||||
public socialLinks: Social[];
|
||||
|
||||
public cleanUrl(url: string): string {
|
||||
try {
|
||||
|
||||
4
src/app/components/timestamp/timestamp.component.html
Normal file
4
src/app/components/timestamp/timestamp.component.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<div class="timestamp" #timestampRef>
|
||||
<h2>{{timestamp.date}}</h2>
|
||||
<span>{{timestamp.description}}</span>
|
||||
</div>
|
||||
73
src/app/components/timestamp/timestamp.component.scss
Normal file
73
src/app/components/timestamp/timestamp.component.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/app/components/timestamp/timestamp.component.ts
Normal file
13
src/app/components/timestamp/timestamp.component.ts
Normal 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
4
src/app/models/about.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface About {
|
||||
about: string;
|
||||
future: string;
|
||||
}
|
||||
@@ -2,4 +2,5 @@ export interface Timestamp {
|
||||
date: number,
|
||||
description: string;
|
||||
featured?: boolean;
|
||||
carrier?: boolean;
|
||||
}
|
||||
|
||||
13
src/app/pipes/carrier.pipe.ts
Normal file
13
src/app/pipes/carrier.pipe.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
13
src/app/pipes/experience.pipe.ts
Normal file
13
src/app/pipes/experience.pipe.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,4 +16,8 @@ export class AnimatorService {
|
||||
});
|
||||
}
|
||||
|
||||
public getAnimationDelay(index: number, multiplier = 150, additional = 0): string {
|
||||
return `${index * multiplier + additional}ms`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {Technology} from "../models/technology";
|
||||
import {Timestamp} from "../models/timestamp";
|
||||
import {Social} from "../models/social";
|
||||
import {Message} from "../models/message";
|
||||
import {About} from "../models/about";
|
||||
|
||||
|
||||
@Injectable({
|
||||
@@ -14,6 +15,13 @@ export class BackendService {
|
||||
|
||||
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() {
|
||||
this.pb = new PocketBase('https://ed168214-77da-44f1-9a61-859abb49edf8.api.leon-hoppe.de');
|
||||
}
|
||||
@@ -23,13 +31,12 @@ export class BackendService {
|
||||
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'];
|
||||
project.status = this.states?.filter(state => state.id == rawProject.status)[0]['name'];
|
||||
|
||||
if (rawProject.languages != undefined) {
|
||||
const languages: Language[] = []
|
||||
@@ -50,7 +57,9 @@ export class BackendService {
|
||||
}
|
||||
|
||||
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[]> {
|
||||
@@ -61,6 +70,10 @@ export class BackendService {
|
||||
];
|
||||
}
|
||||
|
||||
public async getAbout(): Promise<About> {
|
||||
return await this.pb?.collection('about').getFirstListItem('');
|
||||
}
|
||||
|
||||
public async sendMessage(message: Message) {
|
||||
await this.pb?.collection('messages').create(message);
|
||||
}
|
||||
|
||||
32
src/app/services/seo.service.ts
Normal file
32
src/app/services/seo.service.ts
Normal 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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
selector: 'app-about',
|
||||
templateUrl: './about.component.html',
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<div class="hider" [ngClass]="{'mobile': device.isMobile()}">
|
||||
<div class="form-wrapper" [ngClass]="{'mobile': device.isMobile()}">
|
||||
<div class="hider">
|
||||
|
||||
<div class="form-wrapper">
|
||||
<form [formGroup]="form" (ngSubmit)="sendMessage()">
|
||||
<section id="contact-info">
|
||||
<h1>Kontakt</h1>
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
.hider {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
&.mobile {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.form-wrapper {
|
||||
@@ -64,8 +60,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
@media screen and (max-width: theme.$mobile-width) {
|
||||
.hider {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.form-wrapper {
|
||||
display: block;
|
||||
height: max-content;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {FormControl, FormGroup, Validators} from "@angular/forms";
|
||||
import Swal from 'sweetalert2/dist/sweetalert2.js';
|
||||
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({
|
||||
selector: 'app-contact',
|
||||
@@ -17,7 +17,10 @@ export class ContactComponent {
|
||||
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() {
|
||||
if (!this.form.valid) return;
|
||||
@@ -29,12 +32,7 @@ export class ContactComponent {
|
||||
});
|
||||
this.form.reset();
|
||||
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Nachricht gesendet',
|
||||
showConfirmButton: false,
|
||||
timer: 1500
|
||||
});
|
||||
this.snackbar.open("Nachricht gesendet!", undefined, {duration: 2000});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<section id="hero" [ngClass]="{'mobile': deviceService.isMobile()}">
|
||||
<section id="hero">
|
||||
<div class="artwork">
|
||||
<div class="circle big-circle"></div>
|
||||
<div class="circle small-circle"></div>
|
||||
<div class="circle image"></div>
|
||||
</div>
|
||||
<h1>
|
||||
<span>Hallo, ich bin Leon Hoppe,</span><br>
|
||||
full stack developer
|
||||
<span id="welcome">Hallo, ich bin Leon Hoppe,</span><br>
|
||||
<span id="jobs">{{jobs.display}}</span>
|
||||
</h1>
|
||||
<p>
|
||||
Auf dieser Seite erfahren Sie, an welchen Projekten ich bereits gearbeitet habe,<br>
|
||||
@@ -15,15 +15,15 @@
|
||||
<a href="#projects">Mehr erfahren</a>
|
||||
</section>
|
||||
|
||||
<section id="projects" class="home-section" [ngClass]="{'mobile': deviceService.isMobile()}">
|
||||
<section id="projects" class="home-section">
|
||||
<h1 class="title">Projekte</h1>
|
||||
<a routerLink="/projects">alle ansehen</a>
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<section id="technologies" class="home-section" [ngClass]="{'mobile': deviceService.isMobile()}">
|
||||
<section id="technologies" class="home-section">
|
||||
<h1 class="title">Technologien</h1>
|
||||
<a routerLink="/technologies">mehr erfahren</a>
|
||||
<div class="technologies-wrapper">
|
||||
@@ -31,25 +31,12 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="about" class="home-section" [ngClass]="{'mobile': deviceService.isMobile()}">
|
||||
<section id="about" class="home-section">
|
||||
<h1 class="title">Über mich</h1>
|
||||
<a routerLink="/about">mehr erfahren</a>
|
||||
<div id="timeline" #timelineElement>
|
||||
<div class="timestamp" *ngFor="let timestamp of timeline | featured; let i = index" [ngStyle]="{'--delay': getAnimationDelay(i, 500)}">
|
||||
<h2>{{timestamp.date}}</h2>
|
||||
<span>{{timestamp.description}}</span>
|
||||
</div>
|
||||
<div class="timeline" #timelineElement>
|
||||
<app-timestamp *ngFor="let timestamp of timeline | featured; let i = index" [timestamp]="timestamp" [ngStyle]="{'--delay': animator.getAnimationDelay(i, 500)}" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="footer" class="home-section" [ngClass]="{'mobile': deviceService.isMobile()}">
|
||||
<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>
|
||||
<app-footer />
|
||||
|
||||
@@ -15,11 +15,26 @@
|
||||
font-size: 45px;
|
||||
line-height:70px;
|
||||
|
||||
span {
|
||||
#welcome {
|
||||
background: theme.$gradient;
|
||||
background-clip: text;
|
||||
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 {
|
||||
@@ -41,25 +56,6 @@
|
||||
|
||||
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 {
|
||||
@@ -79,13 +75,6 @@
|
||||
animation: fade-in 250ms forwards;
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
#projects-wrapper {
|
||||
margin-top: 30px;
|
||||
gap: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#technologies {
|
||||
@@ -94,152 +83,44 @@
|
||||
|
||||
#about {
|
||||
margin-top: 150px;
|
||||
}
|
||||
|
||||
#timeline {
|
||||
display: flex;
|
||||
@media screen and (max-width: theme.$mobile-width) {
|
||||
#hero {
|
||||
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-wrapper {
|
||||
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;
|
||||
gap: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
&.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;
|
||||
@keyframes blink {
|
||||
0% {
|
||||
opacity: 1;
|
||||
|
||||
span {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
top: 5px;
|
||||
left: -5px;
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:before {
|
||||
top: 5px;
|
||||
left: 1px;
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
&:last-of-type:before {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:first-of-type:before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
*:not(.footer-title) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes timestamp-in {
|
||||
from {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
width: 100%;
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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 {BackendService} from "../../services/backend.service";
|
||||
import {AnimatorService} from "../../services/animator.service";
|
||||
import {Timestamp} from "../../models/timestamp";
|
||||
import {SeoService} from "../../services/seo.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
@@ -20,10 +20,16 @@ export class HomeComponent implements OnInit, AfterViewInit {
|
||||
public timeline: Timestamp[];
|
||||
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 {
|
||||
return `${index * multiplier}ms`;
|
||||
public constructor(private backend: BackendService, public animator: AnimatorService, private seo: SeoService) {
|
||||
setInterval(this.handleJobsAnimation.bind(this), 50);
|
||||
seo.setDefaults();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -38,4 +44,23 @@ export class HomeComponent implements OnInit, AfterViewInit {
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<section class="home-section" [ngClass]="{'mobile': deviceService.isMobile()}">
|
||||
<section class="home-section">
|
||||
<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)}">
|
||||
<div class="project-wrapper" *ngFor="let project of projects; let i = index" [ngStyle]="{'animation-delay': animator.getAnimationDelay(i)}">
|
||||
<app-project [project]="project" showInfo />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {Project} from "../../models/project";
|
||||
import {DeviceDetectorService} from "ngx-device-detector";
|
||||
import {BackendService} from "../../services/backend.service";
|
||||
import {AnimatorService} from "../../services/animator.service";
|
||||
import {SeoService} from "../../services/seo.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-projects',
|
||||
@@ -10,14 +11,13 @@ import {BackendService} from "../../services/backend.service";
|
||||
})
|
||||
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 getAnimationDelay(index: number): string {
|
||||
return `${index * 150}ms`;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.projects = await this.backend.getProjects();
|
||||
}
|
||||
|
||||
@@ -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="circle big-circle"></div>
|
||||
<div class="circle small-circle"></div>
|
||||
@@ -10,21 +10,21 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="home-section" id="languages" [ngClass]="{'mobile': deviceService.isMobile()}">
|
||||
<section class="home-section" id="languages">
|
||||
<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()}">
|
||||
<section class="home-section" id="frameworks">
|
||||
<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()}">
|
||||
<section class="home-section" id="additional">
|
||||
<h1 class="title">Zusätzliche Fähigkeiten</h1>
|
||||
<div id="skills-wrapper">
|
||||
<div class="skill" *ngFor="let skill of technologies | skills">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@use "sass:map";
|
||||
|
||||
#tech-projects {
|
||||
margin-top: 200px;
|
||||
margin-top: 50px;
|
||||
position: relative;
|
||||
|
||||
.chart {
|
||||
@@ -28,16 +28,6 @@
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
margin-top: 50px;
|
||||
|
||||
.chart .chart-container {
|
||||
aspect-ratio: 1 / 1;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#additional {
|
||||
@@ -68,8 +58,20 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile #skills-wrapper {
|
||||
@media screen and (max-width: theme.$mobile-width) {
|
||||
#additional #skills-wrapper {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#tech-projects {
|
||||
margin-top: 50px;
|
||||
|
||||
.chart .chart-container {
|
||||
aspect-ratio: 1 / 1;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ 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";
|
||||
import {SeoService} from "../../services/seo.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-technologies',
|
||||
@@ -14,7 +14,10 @@ export class TechnologiesComponent implements AfterViewInit {
|
||||
@ViewChild('chart') chartRef: ElementRef;
|
||||
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() {
|
||||
const projects = await this.backend.getProjects();
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
<meta charset="utf-8">
|
||||
<title>Portfolio von Leon Hoppe</title>
|
||||
<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">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
|
||||
4
src/robots.txt
Normal file
4
src/robots.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://leon-hoppe.de/sitemap.xml
|
||||
23
src/sitemap.xml
Normal file
23
src/sitemap.xml
Normal 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>
|
||||
@@ -1,8 +1,6 @@
|
||||
@use "sass:map";
|
||||
@use "theme";
|
||||
|
||||
@import '@sweetalert2/theme-dark/dark.scss';
|
||||
|
||||
html, body { height: 100vh; }
|
||||
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
|
||||
*, 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 {
|
||||
from {
|
||||
opacity: 0;
|
||||
@@ -113,3 +168,13 @@ mat-drawer > div {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes timestamp-in {
|
||||
from {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ $gradient-straight: linear-gradient($primary, $secondary);
|
||||
|
||||
$padding: 12.5vw;
|
||||
$padding-small: 5vw;
|
||||
$mobile-width: 750px;
|
||||
|
||||
$desc-color: #7c8393;
|
||||
$border-color: #2d2d2d;
|
||||
@@ -66,26 +67,6 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-mdc-text-field-wrapper {
|
||||
background-color: map.get($background, 'background') !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user