feat: reworked portfolio

This commit is contained in:
2026-04-17 23:40:29 +02:00
parent b0e7a71a0c
commit d34b2aa669
71 changed files with 11627 additions and 5197 deletions
+3 -2
View File
@@ -1,6 +1,7 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import type { ComponentFixture } from '@angular/core/testing';
import { TestBed } from '@angular/core/testing';
import {AppComponent} from './app.component';
import { AppComponent } from './app.component';
describe('AppComponent', (): void => {
beforeEach(async (): Promise<void> => {
+5 -7
View File
@@ -1,13 +1,11 @@
import {Component} from '@angular/core'
import {RouterOutlet} from '@angular/router';
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [
RouterOutlet
],
templateUrl: './app.component.html'
imports: [RouterOutlet],
templateUrl: './app.component.html',
})
export class AppComponent {
title: string = "Angular-Portfolio-Seite";
title: string = 'Angular-Portfolio-Seite';
}
+5 -6
View File
@@ -1,12 +1,11 @@
import {mergeApplicationConfig, ApplicationConfig} from '@angular/core';
import {provideServerRendering} from '@angular/platform-server';
import { provideServerRendering } from '@angular/ssr';
import type { ApplicationConfig } from '@angular/core';
import { mergeApplicationConfig } from '@angular/core';
import {appConfig} from './app.config';
import { appConfig } from './app.config';
const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering(),
]
providers: [provideServerRendering()],
};
export const config: ApplicationConfig = mergeApplicationConfig(appConfig, serverConfig);
+14 -12
View File
@@ -1,19 +1,21 @@
import {ApplicationConfig, importProvidersFrom, provideZoneChangeDetection} from '@angular/core';
import {provideRouter, withInMemoryScrolling} from '@angular/router';
import {BrowserModule, provideClientHydration, withEventReplay} from '@angular/platform-browser';
import type { ApplicationConfig } from '@angular/core';
import { importProvidersFrom, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withInMemoryScrolling } from '@angular/router';
import { BrowserModule, provideClientHydration, withEventReplay } from '@angular/platform-browser';
import {routes} from './app.routes';
import {provideAnimations} from '@angular/platform-browser/animations';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({eventCoalescing: true}),
provideRouter(routes, withInMemoryScrolling({
anchorScrolling: 'enabled',
scrollPositionRestoration: 'top'
})),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(
routes,
withInMemoryScrolling({
anchorScrolling: 'enabled',
scrollPositionRestoration: 'top',
}),
),
provideClientHydration(withEventReplay()),
importProvidersFrom(BrowserModule),
provideAnimations()
]
],
};
+6 -7
View File
@@ -1,11 +1,10 @@
import {Routes} from '@angular/router';
import type { Routes } from '@angular/router';
import {HomeComponent} from './home/home.component';
import {ImprintComponent} from './imprint/imprint.component';
import { HomeComponent } from './home/home.component';
import { ImprintComponent } from './imprint/imprint.component';
export const routes: Routes = [
{path: '', component: HomeComponent, pathMatch: 'full'},
{path: 'imprint', component: ImprintComponent, pathMatch: 'full'},
{path: '**', redirectTo: ''}
{ path: '', component: HomeComponent, pathMatch: 'full' },
{ path: 'imprint', component: ImprintComponent, pathMatch: 'full' },
{ path: '**', redirectTo: '' },
];
+8 -10
View File
@@ -1,16 +1,14 @@
<noscript>
<div class="container">
<div class="card">
<h2 id="loading-text" class="warning-text">
Enable javascript for the full experience!
</h2>
<div class="page-shell footer-shell">
<div class="panel noscript-panel">
<h2 id="loading-text" class="warning-text">Enable JavaScript for the full experience.</h2>
</div>
</div>
</noscript>
<footer>
<p>
<a routerLink="/imprint">IMPRINT</a> | &copy; 2023
- {{ currentYear }} {{ globalFields.firstname }} {{ globalFields.lastname }} - All rights reserved.
</p>
<footer class="site-footer">
<div class="footer-inner">
<p>&copy; 2023 - {{ currentYear }} {{ global.firstname }} {{ global.lastname }}</p>
<a routerLink="/imprint">Imprint</a>
</div>
</footer>
+5 -5
View File
@@ -1,6 +1,7 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import type { ComponentFixture } from '@angular/core/testing';
import { TestBed } from '@angular/core/testing';
import {FooterComponent} from './footer.component';
import { FooterComponent } from './footer.component';
describe('FooterComponent', (): void => {
let component: FooterComponent;
@@ -8,9 +9,8 @@ describe('FooterComponent', (): void => {
beforeEach(async (): Promise<void> => {
await TestBed.configureTestingModule({
imports: [FooterComponent]
})
.compileComponents();
imports: [FooterComponent],
}).compileComponents();
fixture = TestBed.createComponent(FooterComponent);
component = fixture.componentInstance;
+7 -9
View File
@@ -1,16 +1,14 @@
import {Component} from '@angular/core';
import {RouterLink} from "@angular/router";
import {globalFields} from '../../global.fields';
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import type { Global } from '../../global.fields';
import { global } from '../../global.fields';
@Component({
selector: 'app-footer',
imports: [
RouterLink
],
templateUrl: './footer.component.html'
imports: [RouterLink],
templateUrl: './footer.component.html',
})
export class FooterComponent {
protected readonly globalFields: typeof globalFields = globalFields;
protected readonly global: Global = global;
protected readonly currentYear: number = new Date().getFullYear();
}
+145 -135
View File
@@ -1,146 +1,156 @@
<section class="container">
<div class="card profile">
<img
ngSrc="Logo.webp"
width="200"
height="200"
priority
fetchpriority="high"
alt="profile picture"
srcset="Logo.webp 1x, Logo.webp 2x"
(click)="openStalkerInfo()"/>
<h1>Hallo! 👋 I'm {{ globalFields.firstname }} {{ globalFields.lastname }}!</h1>
<h3>📍 Austria, Vienna</h3>
<br>
<hr>
<div class="max60">
<div id="short-bio-text" class="hover-items" (click)="toggleBio()">
<fa-icon id="left-arrow" [icon]=" isLongBioShown ? faArrowDown : faArrowRight "></fa-icon>
<a>
<u>
<h3>A young full-stack software developer from Austria.</h3>
</u>
</a>
<fa-icon id="right-arrow" [icon]=" isLongBioShown ? faArrowDown : faArrowLeft "></fa-icon>
<main class="page-shell home-page">
<section class="hero-section panel">
<div class="hero-copy">
<div class="hero-copy-main">
<p class="eyebrow">Portfolio</p>
<h1>
<span class="hero-name-part">{{ global.firstname }}</span>
<span class="hero-name-part">{{ global.lastname }}</span>
</h1>
<p class="hero-intro">
Full-stack development, thoughtful interfaces, and tailored digital products for companies, associations, and private clients.
</p>
</div>
</div>
<div class="long-bio-text" *ngIf="isLongBioShown" @fadeInOut (click)="closeOnOverlayClick($event)">
<p>
My name is {{ globalFields.firstname }}, many people also call me Julie. I'm {{ age }} years old and come from
beautiful Austria.
I travel extensively within Austria, so I spend a great deal of my time on trains.
<br/>
<br/>
My biggest hobby apart from software development is scuba diving. Floating in deep water and only thinking
about the here and now always brings me back to the depths. I try to practice this hobby as often as
possible for fun and my health.
<br/>
<br/>
I currently work mainly for the Viennese company SobIT Gmbh. This company develops software for most
care companies in Austria. - A universal management tool for the care sector.
<br/>
<br/>
I also work as a freelancer for various associations, companies, and private clients. The software sector is in
high demand, and Im fully immersed in planning, programming, and delivering custom solutions upon request.
<br/>
<br/>
<a [href]="contactSafeMail">
<b><u><h3>Just contact me!</h3></u></b>
</a>
</p>
</div>
<div class="hero-actions">
<div class="contact-area hero-contact-area">
<a [href]="contactSafeMail" aria-label="Send mail">
<fa-icon class="fab" [icon]="faEnvelope" size="lg"></fa-icon>
</a>
<hr>
<br>
<a href="tel:{{ global.hrefContactPhone }}" aria-label="Call phone number">
<fa-icon class="fab" [icon]="faPhone" size="lg"></fa-icon>
</a>
<div class="bio grid-container">
<div class="grid-item-start">
<h1>I work with</h1>
<a href="https://discord.com/users/860206216893693973" aria-label="Discord profile">
<fa-icon class="fab" [icon]="faDiscord" size="lg"></fa-icon>
</a>
<a href="https://github.com/fraujulian" aria-label="GitHub profile">
<fa-icon class="fab" [icon]="faGithub" size="lg"></fa-icon>
</a>
<a href="https://www.linkedin.com/in/julian-lechner-98b377356/" aria-label="LinkedIn profile">
<fa-icon class="fab" [icon]="faLinkedin" size="lg"></fa-icon>
</a>
</div>
<div class="hero-action-links">
<a class="action-link" href="#projects">View projects</a>
<a class="action-link" href="#about">About me</a>
</div>
</div>
<div class="grid-item" *ngFor="let bioText of bioTextsList; let i = index" [hidden]="i !== currentIndex">
<h1>{{ bioText }}</h1>
</div>
</div>
<div class="contact-area">
<a [href]="contactSafeMail">
<fa-icon class="fab" [icon]="faEnvelope" size="3x"></fa-icon>
</a>
<a href="tel:{{globalFields.hrefContactTel}}">
<fa-icon class="fab" [icon]="faPhone" size="3x"></fa-icon>
</a>
<a href="https://discord.com/users/860206216893693973">
<fa-icon class="fab" [icon]="faDiscord" size="3x"></fa-icon>
</a>
<a href="https://github.com/fraujulian">
<fa-icon class="fab" [icon]="faGithub" size="3x"></fa-icon>
</a>
<a href="https://www.xing.com/profile/Julian_Lechner03274">
<fa-icon class="fab" [icon]="faXing" size="3x"></fa-icon>
</a>
<a href="https://www.linkedin.com/in/julian-lechner-98b377356/">
<fa-icon class="fab" [icon]="faLinkedin" size="3x"></fa-icon>
</a>
</div>
</div>
</section>
<div id="github_projects">
<div *ngFor="let project of projects">
<div class="container" [class.clickable]="project.Clickable" [class.disabled]="!project.Clickable"
[style.cursor]="project.Clickable ? 'pointer' : 'default'"
(click)="project.Clickable && (project.IsReadmeShown = true)">
<div class="card">
<div class="media">
<div class="media-body">
<a href="{{project.Link}}">
<u><strong class="d-block text-gray-dark">{{ project.Title }}</strong></u>
</a>
<div class="stars">
{{ project.Languages.join(' | ') }}
<ng-container *ngIf="project.Stars != null">
<fa-icon [icon]="faStar"></fa-icon>
{{ project.Stars }}
</ng-container>
</div>
</div>
<p>{{ project.Description }}</p>
<div class="hero-meta">
<div>
<span class="meta-label">my stack</span>
<strong>{{ global.bioTextsList[currentIndex] }}</strong>
</div>
</div>
</div>
</div>
</div>
<div class="hero-visual">
@if (currentPortraitHighlight) {
<div class="portrait-highlight">
<button
type="button"
class="portrait-highlight-button"
(click)="showNextPortraitHighlight()"
[attr.aria-label]="'Show next highlight image'"
[title]="portraitHighlights.length > 1 ? 'Show next image' : 'Single image'"
>
<div class="portrait-media" [class.portrait-media-switching]="isPortraitSwitching">
<img
class="hero-feature-image"
[src]="currentPortraitHighlight.image"
width="560"
height="420"
[alt]="global.firstname + ' highlight'"
/>
@if (currentPortraitHighlightIndex === 0) {
<span class="portrait-click-hint">Click me!</span>
}
<span class="portrait-caption">{{ currentPortraitHighlight.text }}</span>
</div>
</button>
</div>
}
</div>
</section>
<section id="about" class="story-section panel">
<div class="section-heading">
<p class="eyebrow">About me</p>
<h2>Direct communication, solid implementation, and software that stays understandable.</h2>
</div>
<div class="story-grid">
<div class="story-copy">
<p>
My name is {{ global.firstname }}, many people also call me Julie. I'm {{ age }} years old and come from Austria, where I spend a
lot of time traveling by train between projects, meetings, and clients.
</p>
<p>
I work across modern web technologies and practical business software. My focus is not on noise or unnecessary complexity, but on
systems that are stable, clear, and actually useful in day-to-day work.
</p>
</div>
<div class="story-detail">
<button class="detail-toggle" type="button" (click)="toggleBio()">
<span>{{ isLongBioShown ? 'Hide extended profile' : 'Read extended profile' }}</span>
<fa-icon [icon]="isLongBioShown ? faArrowDown : faArrowRight"></fa-icon>
</button>
@if (isLongBioShown) {
<div class="detail-panel detail-panel-enter">
<p>
My biggest hobby apart from software development is scuba diving. Floating in deep water and only thinking about the here and
now always brings me back to a calmer pace.
</p>
<p>
I currently work mainly for the Viennese company SobIT GmbH and also take on freelance projects for companies, associations,
and private clients. The common thread is always the same: practical software, well delivered.
</p>
<a class="inline-link" [href]="contactSafeMail">Get in touch</a>
</div>
}
</div>
</div>
</section>
<section id="projects" class="projects-section">
<div class="section-heading">
<p class="eyebrow">Selected Projects</p>
<h2>A compact look at products, platforms, and systems I have helped shape.</h2>
</div>
<div class="project-list">
@for (project of projects; track project.link + '-' + $index; let i = $index) {
<article class="panel project-entry" [class.project-entry-reverse]="i % 2 === 1">
@if (project.icon) {
<img
class="project-icon"
[class.project-icon-circle]="project.CircleIcon !== false"
[src]="project.icon"
[alt]="project.title + ' icon'"
/>
}
<div class="project-copy">
<div class="project-header">
<div>
<a class="project-link" href="{{ project.link }}">{{ project.title }}</a>
</div>
<div class="project-languages">{{ project.languages.join(' · ') }}</div>
</div>
<p class="project-description">{{ project.description }}</p>
</div>
</article>
}
</div>
</section>
</main>
<app-footer ngSkipHydration></app-footer>
<div *ngFor="let project of projects">
<div *ngIf="project.IsReadmeShown" class="modal github-modal">
<div class="modal github-modal card big-card">
<a class="modalClose" (click)="project.IsReadmeShown = false"></a>
<a href="{{project.Link}}">Visit on GitHub</a>
<br>
<br>
<div [innerHTML]="project.Readme"></div>
</div>
</div>
</div>
<div *ngIf="isApiRateLimitExceeded" class="modal github-modal">
<div class="modal github-modal card mini-card">
<a class="modalClose" (click)="isApiRateLimitExceeded = false"></a>
<br>
<br>
<h1 style="text-align: center">You have exceeded the today's github api request limits!</h1>
</div>
</div>
+5 -5
View File
@@ -1,6 +1,7 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import type { ComponentFixture } from '@angular/core/testing';
import { TestBed } from '@angular/core/testing';
import {HomeComponent} from './home.component';
import { HomeComponent } from './home.component';
describe('HomeComponent', (): void => {
let component: HomeComponent;
@@ -8,9 +9,8 @@ describe('HomeComponent', (): void => {
beforeEach(async (): Promise<void> => {
await TestBed.configureTestingModule({
imports: [HomeComponent]
})
.compileComponents();
imports: [HomeComponent],
}).compileComponents();
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
+71 -196
View File
@@ -1,148 +1,63 @@
import {
Component,
NgZone,
OnDestroy,
OnInit,
} from '@angular/core';
import {trigger, transition, style, animate} from '@angular/animations';
import {faArrowRight, faArrowLeft, faArrowDown, faStar, faEnvelope, faPhone} from '@fortawesome/free-solid-svg-icons';
import {
faDiscord,
faGithub,
faXing,
faLinkedin
} from '@fortawesome/free-brands-svg-icons';
import {NgForOf, NgIf, NgOptimizedImage} from '@angular/common';
import {FaIconComponent, IconDefinition} from '@fortawesome/angular-fontawesome';
import {DomSanitizer, SafeUrl} from '@angular/platform-browser';
import {interval, Subscription} from 'rxjs';
import MarkdownIt from 'markdown-it';
import type { OnDestroy, OnInit } from '@angular/core';
import { Component, inject, NgZone } from '@angular/core';
import { faArrowRight, faArrowDown, faEnvelope, faPhone } from '@fortawesome/free-solid-svg-icons';
import { faDiscord, faGithub, faLinkedin } from '@fortawesome/free-brands-svg-icons';
import type { IconDefinition } from '@fortawesome/angular-fontawesome';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { DomSanitizer } from '@angular/platform-browser';
import type { SafeUrl } from '@angular/platform-browser';
import type { Subscription } from 'rxjs';
import { interval } from 'rxjs';
import {globalFields} from '../../global.fields';
import {FooterComponent} from '../footer/footer.component';
import {MatDialog} from '@angular/material/dialog';
import {StalkerComponent} from '../stalker/stalker.component';
interface Project {
Link: string;
Title: string;
FullName?: string;
Description: string;
Languages: string[];
DefaultBranch?: string;
Stars?: number;
IsReadmeShown: boolean;
Readme?: string;
Clickable?: boolean;
}
import type { Global, PortraitHighlight, PortfolioProject } from '../../global.fields';
import { global } from '../../global.fields';
import { FooterComponent } from '../footer/footer.component';
@Component({
selector: 'app-home',
standalone: true,
imports: [
NgOptimizedImage,
FooterComponent,
NgIf,
FaIconComponent,
NgForOf
],
imports: [FooterComponent, FaIconComponent],
templateUrl: './home.component.html',
animations: [
trigger('fadeInOut', [
transition(':enter', [
style({opacity: 0}),
animate('300ms ease-in', style({opacity: 1}))
]),
transition(':leave', [
animate('300ms ease-out', style({opacity: 0}))
])
])
]
})
export class HomeComponent implements OnInit, OnDestroy {
protected readonly globalFields: typeof globalFields = globalFields;
private readonly sanitizer = inject(DomSanitizer);
private readonly zone = inject(NgZone);
private preloadedImages: HTMLImageElement[] = [];
protected readonly age: number | string = this.calculateAge('2009-03-03');
protected readonly global: Global = global;
protected readonly age: number | string = this.calculateAge(global.birthdate);
protected isLongBioShown: boolean = false;
protected isApiRateLimitExceeded: boolean = false;
protected readonly faArrowRight: IconDefinition = faArrowRight;
protected readonly faArrowLeft: IconDefinition = faArrowLeft;
protected readonly faArrowDown: IconDefinition = faArrowDown;
protected readonly faStar: IconDefinition = faStar;
protected readonly faEnvelope: IconDefinition = faEnvelope;
protected readonly faPhone: IconDefinition = faPhone;
readonly faDiscord: IconDefinition = faDiscord;
readonly faGithub: IconDefinition = faGithub;
readonly faXing: IconDefinition = faXing;
readonly faLinkedin: IconDefinition = faLinkedin;
protected readonly contactSafeMail: SafeUrl;
protected readonly personalInformation: string = 'You found an easter egg, why are you here!?!\nYou want to know more personal things about me? - Then you will find your information here!:\nI am currently building an independent foundation for my financial freedom. I am establishing my own software company specializing in client-based projects.\nMy motto, “No stress in life,” guides me to avoid unnecessary pressure.\nI identify as pansexual, meaning I am attracted to individuals regardless of their gender. - I am in a committed relationship. (I have been a man since the very beginning of my life. Don\'t be confused about my username, it\'s just a name!)';
readonly bioTextsList: string[] = [
'C#',
'.NET',
'.NET Framework',
'WPF',
'Avalonia',
'NodeJS',
'Angular',
'TypeScript',
'JavaScript',
'Java',
'HTML',
'(S)CSS',
'Github',
'Gitlab',
'Azure DevOps',
'Ubuntu',
'Debian',
'SSMS',
'MariaDB',
'MySQL',
'Grandle'
];
protected readonly portraitHighlights: PortraitHighlight[] = global.portraitHighlights;
protected currentPortraitHighlightIndex: number = 0;
protected isPortraitSwitching: boolean = false;
protected currentIndex = 0;
protected readonly projects: PortfolioProject[] = global.projects;
private sub!: Subscription;
protected projects: Project[] = [
{
Link: "https://www.synradio.de/",
Title: "SynRadio",
Description: "🖥️ An internet radio station that is available on various media.",
Languages: ["TypeScript", "JavaScript", "HTML", "CSS"],
Clickable: false,
IsReadmeShown: false
},
{
Link: "https://www.synhost.de/",
Title: "SynHost",
Description: "❓ A support system integrating various platforms and media to offer employees a unified overview.",
Languages: ["TypeScript"],
Clickable: false,
IsReadmeShown: false
}
]
constructor(private _sanitizer: DomSanitizer, private zone: NgZone, private dialog: MatDialog) {
this.contactSafeMail = this._sanitizer.bypassSecurityTrustUrl(`mailto:${globalFields.contactMail}`);
constructor() {
this.contactSafeMail = this.sanitizer.bypassSecurityTrustUrl(`mailto:${global.contactMail}`);
}
ngOnInit(): void {
this.preloadImageAssets();
this.zone.runOutsideAngular((): void => {
this.sub = interval(1000).subscribe((): void => {
this.zone.run((): void => {
this.currentIndex = (this.currentIndex + 1) % this.bioTextsList.length;
this.currentIndex = (this.currentIndex + 1) % this.global.bioTextsList.length;
});
});
setTimeout((): void => {
this.zone.run((): void => {
this.fillProjects();
});
}, 1000);
});
}
@@ -150,94 +65,54 @@ export class HomeComponent implements OnInit, OnDestroy {
this.sub.unsubscribe();
}
openStalkerInfo(): void {
this.dialog.open(StalkerComponent, {
width: '420px',
data: {message: this.personalInformation}
});
}
private fillProjects(): void {
const sortOnKey: <T>(key: keyof T, descending: boolean) => (a: T, b: T) => number = <T>(key: keyof T, descending: boolean): (a: T, b: T) => number => {
return (a: T, b: T): number => {
const aValue = a[key] as number;
const bValue = b[key] as number;
return descending ? bValue - aValue : aValue - bValue;
};
};
fetch(`https://api.github.com/users/${this.globalFields.githubUsername}/repos?per_page=${this.globalFields.numberOfLoadedRepositories}`)
.then(async (rawResponse: Response): Promise<any> => {
if (rawResponse.status === 403) {
const data = await rawResponse.json();
if (data.message && data.documentation_url) {
this.isApiRateLimitExceeded = true;
}
return new Error('403 Forbidden');
}
return rawResponse.json();
})
.then((allProjects: any): void => {
allProjects = allProjects.sort(sortOnKey("stargazers_count", true));
allProjects.sort(sortOnKey("stargazers_count", true)).forEach((currentProject: any): void => {
if (currentProject === undefined || currentProject.fork === true) return;
if (currentProject.description == null) currentProject.description = "";
if (currentProject.name === "FrauJulian") return;
let newProject: Project = {
Link: "",
Title: "",
FullName: "",
Description: "",
Languages: [],
DefaultBranch: "",
Stars: 0,
Clickable: true,
IsReadmeShown: false,
Readme: ""
};
if (currentProject.language != null) {
fetch(currentProject.languages_url)
.then((rawResponse: Response): Promise<any> => rawResponse.json())
.then((languages: any): void => {
newProject.Languages = Object.keys(languages);
});
} else {
newProject.Languages = [""];
}
fetch(`https://raw.githubusercontent.com/${currentProject.full_name}/${currentProject.default_branch}/README.md`)
.then((rawResponse: Response): Promise<any> => rawResponse.text())
.then((readmeText: string): void => {
newProject.Readme = new MarkdownIt({html: true, linkify: true, typographer: true}).render(readmeText);
});
currentProject.name = currentProject.name
.replaceAll("_", " ")
.replaceAll("-", " ");
newProject.Link = currentProject.html_url;
newProject.Title = currentProject.name;
newProject.FullName = currentProject.full_name;
newProject.Description = currentProject.description;
newProject.DefaultBranch = currentProject.default_branch;
newProject.Stars = currentProject.stargazers_count;
newProject.IsReadmeShown = false;
this.projects.push(newProject);
});
});
}
protected toggleBio(): void {
this.isLongBioShown = !this.isLongBioShown;
}
protected closeOnOverlayClick(evt: MouseEvent): void {
if ((evt.target as HTMLElement).classList.contains('long-bio-overlay')) {
this.isLongBioShown = false;
protected get currentPortraitHighlight(): PortraitHighlight | null {
return this.portraitHighlights[this.currentPortraitHighlightIndex] ?? null;
}
protected showNextPortraitHighlight(): void {
if (this.portraitHighlights.length <= 1 || this.isPortraitSwitching) {
return;
}
this.isPortraitSwitching = true;
setTimeout((): void => {
this.currentPortraitHighlightIndex =
(this.currentPortraitHighlightIndex + 1) % this.portraitHighlights.length;
}, 125);
setTimeout((): void => {
this.isPortraitSwitching = false;
}, 250);
}
private preloadImageAssets(): void {
if (typeof window === 'undefined') {
return;
}
const projectIcons: string[] = this.projects
.map((project: PortfolioProject): string | undefined => project.icon)
.filter((icon: string | undefined): icon is string => Boolean(icon));
const uniqueImageUrls: string[] = Array.from(
new Set<string>([
...this.portraitHighlights.map((highlight: PortraitHighlight): string => highlight.image),
...projectIcons,
]),
);
this.preloadedImages = uniqueImageUrls.map((url: string): HTMLImageElement => {
const image = new Image();
image.decoding = 'async';
image.loading = 'eager';
image.src = url;
return image;
});
}
private calculateAge(birthDateString: string): number | string {
+78 -69
View File
@@ -1,76 +1,85 @@
<div class="container">
<div class="card profile">
<main class="page-shell imprint-page">
<section class="panel legal-hero">
<a class="back-link" routerLink="">
<fa-icon [icon]="faArrowLeft"></fa-icon>
<span>Back to portfolio</span>
</a>
<h1>Imprint</h1>
<p class="eyebrow">Legal</p>
<h1>Imprint & Privacy</h1>
<p class="legal-intro">
Legal disclosure for this website under Austrian law, in particular Section 5 ECG (E-Commerce Act) and Section 25 Austrian Media Act.
</p>
</section>
<h2>Information according to §5 ECG</h2>
<section class="legal-grid">
<article class="panel legal-card">
<p class="eyebrow">Provider</p>
<p class="legal-provider-name">{{ global.firstname }} {{ global.lastname }}</p>
<p class="legal-row">{{ global.address.street }} {{ global.address.houseNumber }}</p>
<p class="legal-row">{{ global.address.zip }} {{ global.address.city }}</p>
<p class="legal-row">{{ global.address.country }}</p>
</article>
{{ globalFields.firstname }} {{ globalFields.lastname }} <br>
{{ street }} {{ houseNumber }} <br>
{{ zip }} {{ city }} <br>
{{ country }} <br>
<article class="panel legal-card">
<p class="eyebrow">Contact</p>
<p class="legal-row">
Phone:
<a href="tel:{{ global.hrefContactPhone }}">{{ global.contactPhone }}</a>
</p>
<p class="legal-row">
E-mail:
<a [href]="contactSafeMail">{{ global.contactMail }}</a>
</p>
<p class="legal-row">
Abuse:
<a [href]="abuseSafeMail">{{ global.abuseMail }}</a>
</p>
</article>
</section>
<br>
<h4>Contact:</h4>
Phone: <b><u><a href="tel:{{globalFields.hrefContactTel}}">{{ globalFields.contactPhone }}</a></u></b> <br>
E-Mail: <b><u><a [href]="contactSafeMail">{{ globalFields.contactMail }}</a></u></b>
<br>
<h4>Abuse Contact:</h4>
E-Mail: <b><u><a [href]="abuseSafeMail">{{ abuseMail }}</a></u></b>
<br>
<h2>Disclaimer:</h2>
<h3>Liability for links</h3>
Our website contains links to external third-party websites over whose content we have no influence. Therefore, we
cannot accept any liability for this third-party content. The respective provider or operator of the pages is
always responsible for the content of the linked pages. The linked pages were checked for possible legal
violations at the time of linking. Illegal content was not recognizable at the time of linking. However, permanent
monitoring of the content of the linked pages is not reasonable without concrete evidence of an infringement. If
we become aware of any legal infringements, we will remove such links immediately.
<br>
<br>
<h3>Copyright</h3>
The content and works created by the site operators on these pages are subject to German copyright law.
Duplication, processing, distribution and any form of commercialization of such material beyond the scope of the
copyright law shall require the prior written consent of its respective author or creator. Downloads and copies of
this site are only permitted for private, non-commercial use. Insofar as the content on this site was not created
by the operator, the copyrights of third parties are respected. In particular, third-party content is identified
as such. Should you nevertheless become aware of a copyright infringement, please inform us accordingly. If we
become aware of any infringements, we will remove such content immediately.
<br>
<br>
<h3>Data privacy</h3>
The use of our website is generally possible without providing personal data. Insofar as personal data (e.g. name,
address or e-mail addresses) is collected on our website, this is always done on a voluntary basis as far as
possible. This data will not be passed on to third parties without your express consent.
We would like to point out that data transmission over the Internet (e.g. when communicating by e-mail) may be
subject to security vulnerabilities. Complete protection of data against access by third parties is not possible.
We hereby expressly prohibit the use of contact data published within the scope of the imprint obligation by third
parties for sending unsolicited advertising and information material. The operators of the website expressly
reserve the right to take legal action in the event of the unsolicited sending of advertising information, such as
spam e-mails.
<div class="max60">
<div id="short-desc" class="hover-items" routerLink="">
<fa-icon [icon]="faArrowRight"></fa-icon>
<a>
<u>
<h2>Back</h2>
</u>
</a>
<fa-icon [icon]="faArrowLeft"></fa-icon>
</div>
<section class="panel legal-copy">
<div class="legal-block">
<h2>Scope of Website</h2>
<p>This website is a personal portfolio and a contact point for software engineering and related digital services.</p>
</div>
</div>
</div>
<div class="legal-block">
<h2>Media Disclosure (Section 25 Austrian Media Act)</h2>
<p>
Media owner and publisher: {{ global.firstname }} {{ global.lastname }}. Editorial policy: Information about projects, technical
services, and professional activities in software development.
</p>
</div>
<div class="legal-block">
<h2>Liability for Content and External Links</h2>
<p>
Great care is taken when creating and maintaining this website. However, no liability is accepted for external websites linked from
this page. The respective operators are solely responsible for linked content.
</p>
</div>
<div class="legal-block">
<h2>Copyright</h2>
<p>
All content on this website (including text, images, and layout) is protected by copyright law. Any use beyond statutory exceptions
requires prior written permission.
</p>
</div>
<div class="legal-block">
<h2>Privacy (GDPR / DSGVO)</h2>
<p>
This website can generally be used without submitting personal data. If you contact me by e-mail, your data is processed only to
handle your request and is not shared without a valid legal basis.
</p>
<p>
You may exercise your data subject rights (access, rectification, erasure, restriction, objection) via the contact details listed
above.
</p>
</div>
</section>
</main>
<app-footer ngSkipHydration></app-footer>
+4 -4
View File
@@ -1,4 +1,5 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import type { ComponentFixture } from '@angular/core/testing';
import { TestBed } from '@angular/core/testing';
import { ImprintComponent } from './imprint.component';
@@ -8,9 +9,8 @@ describe('ImprintComponent', (): void => {
beforeEach(async (): Promise<void> => {
await TestBed.configureTestingModule({
imports: [ImprintComponent]
})
.compileComponents();
imports: [ImprintComponent],
}).compileComponents();
fixture = TestBed.createComponent(ImprintComponent);
component = fixture.componentInstance;
+18 -26
View File
@@ -1,41 +1,33 @@
import {Component, OnInit} from '@angular/core';
import {FooterComponent} from '../footer/footer.component';
import {RouterLink} from '@angular/router';
import {globalFields} from '../../global.fields';
import {DomSanitizer, SafeUrl} from '@angular/platform-browser';
import {FaIconComponent, IconDefinition} from '@fortawesome/angular-fontawesome';
import {faArrowDown, faArrowLeft, faArrowRight} from '@fortawesome/free-solid-svg-icons';
import type { OnInit } from '@angular/core';
import { Component, inject } from '@angular/core';
import { RouterLink } from '@angular/router';
import { DomSanitizer } from '@angular/platform-browser';
import type { SafeUrl } from '@angular/platform-browser';
import type { IconDefinition } from '@fortawesome/angular-fontawesome';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons';
import type { Global } from '../../global.fields';
import { global } from '../../global.fields';
import { FooterComponent } from '../footer/footer.component';
@Component({
selector: 'app-imprint',
imports: [
FooterComponent,
FaIconComponent,
RouterLink
],
templateUrl: './imprint.component.html'
imports: [FooterComponent, FaIconComponent, RouterLink],
templateUrl: './imprint.component.html',
})
export class ImprintComponent implements OnInit {
protected readonly globalFields: typeof globalFields = globalFields;
private readonly sanitizer = inject(DomSanitizer);
protected readonly street: string = 'Ulmenstraße';
protected readonly houseNumber: number = 9;
protected readonly zip: number = 3380;
protected readonly city: string = 'Pöchlarn';
protected readonly country: string = 'Austria';
protected readonly abuseMail: string = 'abuse@lechner-systems.at';
protected readonly global: Global = global;
protected contactSafeMail!: SafeUrl;
protected abuseSafeMail!: SafeUrl;
protected readonly faArrowRight: IconDefinition = faArrowRight;
protected readonly faArrowLeft: IconDefinition = faArrowLeft;
constructor(private _sanitizer: DomSanitizer) {
}
ngOnInit(): void {
this.contactSafeMail = this._sanitizer.bypassSecurityTrustUrl(`mailto:${globalFields.contactMail}`);
this.abuseSafeMail = this._sanitizer.bypassSecurityTrustUrl(`mailto:${this.abuseMail}`);
this.contactSafeMail = this.sanitizer.bypassSecurityTrustUrl(`mailto:${global.contactMail}`);
this.abuseSafeMail = this.sanitizer.bypassSecurityTrustUrl(`mailto:${this.global.abuseMail}`);
}
}
-9
View File
@@ -1,9 +0,0 @@
<h1 mat-dialog-title>STALKER!!!</h1>
<div mat-dialog-content *ngFor="let line of lines">
{{ line }} <br>
</div>
<div mat-dialog-actions>
<button mat-button mat-dialog-close>Close</button>
</div>
-23
View File
@@ -1,23 +0,0 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {StalkerComponent} from './stalker.component';
describe('StalkerComponent', (): void => {
let component: StalkerComponent;
let fixture: ComponentFixture<StalkerComponent>;
beforeEach(async (): Promise<void> => {
await TestBed.configureTestingModule({
imports: [StalkerComponent]
})
.compileComponents();
fixture = TestBed.createComponent(StalkerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', (): void => {
expect(component).toBeTruthy();
});
});
-30
View File
@@ -1,30 +0,0 @@
import {Component, Inject} from '@angular/core';
import {
MAT_DIALOG_DATA,
MatDialogActions,
MatDialogClose,
MatDialogContent,
MatDialogTitle
} from '@angular/material/dialog';
import {MatButton} from '@angular/material/button';
import {NgForOf, CommonModule} from '@angular/common';
@Component({
selector: 'app-stalker',
imports: [
MatDialogTitle,
MatDialogActions,
MatDialogContent,
MatButton,
MatDialogClose,
CommonModule
],
templateUrl: './stalker.component.html'
})
export class StalkerComponent {
lines: string[];
constructor(@Inject(MAT_DIALOG_DATA) public data: { message: string }) {
this.lines = (data.message ?? '').split('\n');
}
}