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",
|
"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
47
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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) {}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
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">
|
<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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
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,
|
date: number,
|
||||||
description: string;
|
description: string;
|
||||||
featured?: boolean;
|
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 {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);
|
||||||
}
|
}
|
||||||
|
|||||||
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({
|
@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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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 {
|
@media screen and (max-width: theme.$mobile-width) {
|
||||||
display: flex;
|
#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;
|
margin-top: 30px;
|
||||||
|
gap: 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 {
|
@keyframes blink {
|
||||||
box-sizing: border-box;
|
0% {
|
||||||
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;
|
opacity: 1;
|
||||||
|
|
||||||
span {
|
|
||||||
margin-bottom: 50px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
50% {
|
||||||
top: 5px;
|
opacity: 0;
|
||||||
left: -5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
100% {
|
||||||
top: 5px;
|
opacity: 1;
|
||||||
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%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
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 "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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user