feat: reworked portfolio
This commit is contained in:
@@ -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> => {
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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
@@ -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()
|
||||
]
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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: '' },
|
||||
];
|
||||
|
||||
|
||||
@@ -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> | © 2023
|
||||
- {{ currentYear }} {{ globalFields.firstname }} {{ globalFields.lastname }} - All rights reserved.
|
||||
</p>
|
||||
<footer class="site-footer">
|
||||
<div class="footer-inner">
|
||||
<p>© 2023 - {{ currentYear }} {{ global.firstname }} {{ global.lastname }}</p>
|
||||
<a routerLink="/imprint">Imprint</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 I’m 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>
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user