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
+31
View File
@@ -0,0 +1,31 @@
# Git- und Projekt-Metadaten werden nicht in den Docker-Build-Kontext kopiert
.git
.github
.husky
# Lokale Editor-/IDE-Dateien
.idea
.vscode
# Build- und Tool-Caches
.angular
node_modules
dist
.npm-cache
coverage
.eslintcache
# Lokale Log- und Debug-Dateien
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
*.log
*.tsbuildinfo
# Lokale Secrets und Umgebungsdateien
.env
.env.*
backend/.env
backend/.env.*
+2 -10
View File
@@ -1,17 +1,9 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = crlf
insert_final_newline = true
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
ij_typescript_use_double_quotes = false
[*.md]
max_line_length = off
trim_trailing_whitespace = false
+42
View File
@@ -0,0 +1,42 @@
name: 🐛 Bug Report
description: Report a bug or unexpected behavior
title: '🐛 [BUG]: '
body:
- type: textarea
id: information
attributes:
label: Information
description: Provide all information about the issue.
placeholder: 'What happened? What did you expect to happen?'
validations:
required: false
- type: textarea
id: steps
attributes:
label: Steps to Reproduce
description: Step-by-step instructions to reproduce the issue.
placeholder: |
1. Go to ...
2. Click on ...
3. Observe ...
validations:
required: false
- type: textarea
id: logs
attributes:
label: Logs or Error Messages
description: Paste any relevant console output, stack traces, or screenshots.
placeholder: 'Console output, stacktrace, or screenshots here'
validations:
required: false
- type: input
id: occurrence
attributes:
label: Occurrence Version
description: Specify in which version the issue occurred
placeholder: 'v0.1.TTMMJJ-hhmm'
validations:
required: true
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+29
View File
@@ -0,0 +1,29 @@
name: 💡 Enhancement
description: Suggest a new feature or improvement
title: '💡 [ENHANCEMENT]: '
body:
- type: textarea
id: description
attributes:
label: Description
description: Describe the new feature or improvement!
placeholder: 'What problem does it solve or what value does it add?'
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Proposed Solution
placeholder: 'Describe how the feature should work.'
validations:
required: false
- type: input
id: implementation
attributes:
label: Implementation Version
description: Specify in which version the enhancement should be released.
placeholder: 'v0.1'
validations:
required: true
+39
View File
@@ -0,0 +1,39 @@
name: ⚙️ Improvement
description: Suggest an improvement to existing functionality, code quality, or performance
title: '⚙️ [Improvement]: '
body:
- type: textarea
id: context
attributes:
label: Current Situation / Context
description: Describe the current behavior or implementation that could be improved.
placeholder: 'Currently, the system loads all users at once, which slows down response time.'
validations:
required: false
- type: textarea
id: proposal
attributes:
label: Proposed Improvement
description: Describe your proposed change or improvement in detail.
placeholder: 'Implement pagination for user loading to reduce memory usage and response time.'
validations:
required: false
- type: textarea
id: benefits
attributes:
label: Expected Benefits
description: Explain how this improvement will help (e.g., better performance, cleaner code, easier maintenance).
placeholder: 'Faster load times, improved scalability, and reduced database load.'
validations:
required: false
- type: input
id: implementation
attributes:
label: Implementation Version
description: Specify in which version the improvement should be released.
placeholder: 'v0.1'
validations:
required: true
+11
View File
@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'daily'
-40
View File
@@ -1,40 +0,0 @@
name: Build Solution
on:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: checkout Repository
uses: actions/checkout@v4
- name: use NodeJS v24.4.0
uses: actions/setup-node@v4
with:
node-version: '24.4.0'
- name: Install Dependencies
run: npm install
- name: run Build
run: npm run buildProduction
- name: get Version from package.json
id: get_version
run: |
version=$(node -p "require('./package.json').version")
echo "VERSION=$version" >> $GITHUB_ENV
- name: rename dist-Folder
run: mv dist "${{ env.VERSION }}"
- name: compress Version-Folder
run: zip -r "${{ env.VERSION }}-PROD.zip" "${{ env.VERSION }}"
- name: Upload Build-Artefact
uses: actions/upload-artifact@v4
with:
name: ${{ env.VERSION }}
path: ${{ env.VERSION }}
+63
View File
@@ -0,0 +1,63 @@
name: Build Docker Image - Dev
on:
workflow_dispatch:
push:
branches:
- main
env:
REGISTRY: docker.lechner-systems.at
IMAGE_NAME: lechnersystems/ttm
jobs:
docker:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Use Node.js v24.14.0
uses: actions/setup-node@v4
with:
node-version: '24.14.0'
- name: Set build version
run: npm run version:set
- name: Read version from package.json
id: version
run: |
version=$(node -p 'require("./package.json").version')
docker_tag=$version
echo "value=$version" >> "$GITHUB_OUTPUT"
echo "docker_tag=$docker_tag" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
- name: Build and push dev image
uses: docker/build-push-action@v6
with:
context: .
push: true
provenance: false
build-args: |
APP_VERSION=${{ steps.version.outputs.value }}
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
# ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.docker_tag }}
labels: |
org.opencontainers.image.version=${{ steps.version.outputs.value }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.source=https://github.com/${{ github.repository }}
+97
View File
@@ -0,0 +1,97 @@
name: Build Docker Image - Latest
on:
workflow_dispatch:
env:
REGISTRY: docker.lechner-systems.at
IMAGE_NAME: lechnersystems/ttm
jobs:
docker:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Use Node.js v24.14.0
uses: actions/setup-node@v4
with:
node-version: '24.14.0'
- name: Set build version
run: npm run version:set
- name: Read version from package.json
id: version
run: |
version=$(node -p 'require("./package.json").version')
docker_tag=${version//+/-}
echo "value=$version" >> "$GITHUB_OUTPUT"
echo "docker_tag=$docker_tag" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
- name: Build and push latest image
uses: docker/build-push-action@v6
with:
context: .
push: true
provenance: false
build-args: |
APP_VERSION=${{ steps.version.outputs.value }}
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.docker_tag }}
labels: |
org.opencontainers.image.version=${{ steps.version.outputs.value }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.source=https://github.com/${{ github.repository }}
- name: Create GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: v${{ steps.version.outputs.docker_tag }}
run: |
previous_tag="$(gh api repos/${{ github.repository }}/releases --paginate --jq '.[] | select(.draft == false) | .tag_name' | grep -Fxv "$RELEASE_TAG" | head -n 1 || true)"
if [ -n "$previous_tag" ]; then
commit_messages="$(git log --reverse --format='- %s' "${previous_tag}..${GITHUB_SHA}")"
full_changelog="https://github.com/${{ github.repository }}/compare/${previous_tag}...${RELEASE_TAG}"
else
commit_messages="$(git log --reverse --format='- %s' "${GITHUB_SHA}")"
full_changelog=""
fi
if [ -z "$commit_messages" ]; then
commit_messages='- No commits since the previous release'
fi
{
echo '## Changes'
echo
printf '%s\n' "$commit_messages"
echo
if [ -n "$full_changelog" ]; then
echo "**Full Changelog**: ${full_changelog}"
fi
} > release-notes.md
if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
gh release edit "$RELEASE_TAG" --title "$RELEASE_TAG" --notes-file release-notes.md
else
gh release create "$RELEASE_TAG" --target "${GITHUB_SHA}" --title "$RELEASE_TAG" --notes-file release-notes.md
fi
@@ -1,6 +1,10 @@
name: Build Validation
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened]
@@ -11,16 +15,13 @@ jobs:
- name: checkout Repository
uses: actions/checkout@v4
- name: use NodeJS v22.16.0
- name: use NodeJS v24.14.0
uses: actions/setup-node@v4
with:
node-version: '22.16.0'
node-version: '24.14.0'
- name: Install Dependencies
- name: install Dependencies
run: npm install
- name: run Build DEV
run: npm run buildDevelopment
- name: run Build PROD
run: npm run buildProduction
- name: run Build Validation
run: npm run check
+31 -35
View File
@@ -1,42 +1,38 @@
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
# Paketmanager-Abhaengigkeiten
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Lokale Tool-Caches
.npm-cache
.eslintcache
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
# Generierte Build- und Test-Ausgaben
/dist
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
# Betriebssystem-Dateileichen
.DS_Store
Thumbs.db
# Editor- und IDE-Konfigurationen
/.idea
/.vscode
# Lokale Log- und Debug-Dateien
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
*.log
*.tsbuildinfo
# Lokale Secrets und Umgebungsdateien
.env
.env.*
!.env.example
backend/.env
backend/.env.*
!backend/.env.example
# Angular-CLI-Cache
/.angular
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env sh
npm run fix || exit 1
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env sh
npm run check || exit 1
+39
View File
@@ -0,0 +1,39 @@
# Paketmanager-Abhaengigkeiten werden nicht formatiert
/node_modules
# Lokale Tool-Caches
.npm-cache
.eslintcache
# Generierte Build- und Test-Ausgaben
/dist
/coverage
# Betriebssystem-Dateileichen
.DS_Store
Thumbs.db
# Editor- und IDE-Konfigurationen
/.idea
/.vscode
# Lokale Log- und Debug-Dateien
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
*.log
*.tsbuildinfo
# Lokale Secrets und Umgebungsdateien
.env
.env.*
backend/.env
backend/.env.*
# Angular-CLI-Cache
/.angular
# Generierte Tailwind-Ausgabe wird aus dem Quell-CSS erzeugt
src/assets/Resources/CSS/Tailwind.css
+45
View File
@@ -0,0 +1,45 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"singleQuote": true,
"semi": true,
"trailingComma": "all",
"tabWidth": 2,
"useTabs": false,
"printWidth": 100,
"endOfLine": "auto",
"bracketSpacing": true,
"arrowParens": "always",
"htmlWhitespaceSensitivity": "css",
"overrides": [
{
"files": "*.html",
"options": {
"printWidth": 140
}
},
{
"files": "*.md",
"options": {
"printWidth": 80,
"proseWrap": "always"
}
},
{
"files": "*.json",
"options": {
"printWidth": 120
}
},
{
"files": "*.scss",
"options": {
"printWidth": 120
}
}
]
}
+57
View File
@@ -0,0 +1,57 @@
# Repository Guidelines
## Project Structure & Module Organization
This repository is an Angular 21 SSR application. Main app code lives in `src/`,
with standalone components under `src/app/` such as `home/`, `footer/`, and
`imprint/`. Shared constants are kept in `src/global.fields.ts`. Component tests
sit next to their source files as `*.spec.ts`. Automation and project
maintenance scripts live in `scripts/`. Generated build output goes to `dist/`
and should not be edited manually.
## Build, Test, and Development Commands
- `npm run dev`: generate static assets, then start the Angular dev server.
- `npm run build`: generate assets, create a production build, then copy static
root files.
- `npm run test`: run unit tests with Karma/Jasmine.
- `npm run lint`: run the base ESLint config and the type-aware TypeScript
config.
- `npm run format:check`: verify Prettier formatting without changing files.
- `npm run check`: full CI-style verification (`build`, `lint`, formatting,
version, and text checks).
- `npm run fix`: apply the local autofix workflow, including version placeholder
reset, lint fixes, text cleanup, and Prettier.
## Coding Style & Naming Conventions
Follow `.editorconfig`: UTF-8, spaces, 2-space indentation, final newline, and
trimmed trailing whitespace. Use Prettier for formatting and ESLint for code
quality. Keep Angular file naming consistent: `feature.component.ts`,
`feature.component.html`, and `feature.component.spec.ts`. Prefer PascalCase for
classes and interfaces, camelCase for members, and keep scripts in CommonJS
format as `*.cjs`.
## Testing Guidelines
The project uses Jasmine with Karma via `ng test`. Place tests beside the
implementation file and keep names aligned with the component or module under
test. Run `npm run test` for unit tests and `npm run check` before opening a PR.
No explicit coverage gate is configured, so contributors should add focused
tests for changed behavior.
## Commit & Pull Request Guidelines
Recent history mixes short imperative messages (`Update src/global.fields.ts`)
with lightweight conventional commits
(`feat: pre-render routes and restore scroll`). Prefer concise, imperative
commit subjects and include a scope or prefix when it adds clarity. PRs should
summarize the user-visible change, mention any config or script updates, link
related issues, and include screenshots for UI changes.
## Configuration Tips
Use the placeholder version workflow intentionally: `version:check` expects
`YYDDDhhmm+GIT_HASH`, while build pipelines may resolve it through the scripts
in `scripts/version.cjs`. Keep `package.json` and `package-lock.json` in sync,
and avoid committing manual edits to generated files in `dist/`.
+31
View File
@@ -0,0 +1,31 @@
FROM node:24.14.0-bookworm-slim AS build
WORKDIR /app
ENV HUSKY=0
ARG APP_VERSION
COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund
COPY . .
RUN npm run build
RUN npm prune --omit=dev
FROM node:24.14.0-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
ENV MAIN_PORT=3001
ARG APP_VERSION
ENV APP_VERSION=$APP_VERSION
COPY --from=build --chown=node:node /app/package.json ./package.json
COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/dist ./dist
USER node
EXPOSE 3001
CMD ["npm", "start"]
+10 -3
View File
@@ -1,9 +1,11 @@
# 🖼️ FrauJulian's Portfolio Website
### [🙋‍♂️ Website Preview](https://fraujulian.xyz/)
![grafik](https://github.com/user-attachments/assets/65f1e491-62ab-4b16-a39c-5f04377ce5cb)
## 👂 Languages/Framework/Packages:
- Angular v19
- TypeScript
- SCSS
@@ -28,7 +30,9 @@ or for Production Build
npm run buildProduction
```
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
This will compile your project and store the build artifacts in the `dist/`
directory. By default, the production build optimizes your application for
performance and speed.
### Start compiled Project
@@ -38,7 +42,8 @@ npm run start
## Running unit tests
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
To execute unit tests with the [Karma](https://karma-runner.github.io) test
runner, use the following command:
```bash
nom run test
@@ -51,10 +56,12 @@ npm run updatePackageVersions
```
## 📋 Credits:
~ made by [**FrauJulian**](https://fraujulian.xyz/).
## 🤝 Enjoy?
Give it a star ⭐ on [github](https://github.com/FrauJulian/discord-audio-stream)!
Give it a star ⭐ on
[github](https://github.com/FrauJulian/discord-audio-stream)!
### Greetings from Austria! ⛰️
+40 -15
View File
@@ -20,21 +20,21 @@
"outputPath": "dist",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "src/assets",
"output": "assets"
},
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"node_modules/@angular/material/prebuilt-themes/rose-red.css",
"src/styles.scss"
],
"styles": ["node_modules/@angular/material/prebuilt-themes/rose-red.css", "src/styles.scss"],
"scripts": [],
"server": "src/main.server.ts",
"prerender": true,
@@ -99,23 +99,22 @@
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"polyfills": ["zone.js", "zone.js/testing"],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "src/assets",
"output": "assets"
},
{
"glob": "**/*",
"input": "public",
"output": "public"
}
],
"styles": [
"node_modules/@angular/material/prebuilt-themes/rose-red.css",
"src/styles.scss"
],
"styles": ["node_modules/@angular/material/prebuilt-themes/rose-red.css", "src/styles.scss"],
"scripts": []
}
}
@@ -124,5 +123,31 @@
},
"cli": {
"analytics": false
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
}
+13
View File
@@ -0,0 +1,13 @@
services:
app:
image: docker.lechner-systems.at/lechnersystems/ttm:${IMAGE_TAG}
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
env_file:
- .env
dns:
- 192.168.100.2
ports:
- ${MAIN_PORT}:${MAIN_PORT}
+112
View File
@@ -0,0 +1,112 @@
const angularEslint = require('angular-eslint');
/** @type {import('eslint').Linter.FlatConfig[]} */
const config = [
{
ignores: ['dist/**', 'node_modules/**', '.angular/**', 'backend/sql/**'],
},
{
files: ['**/*.{ts,tsx,js,cjs,mjs}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
parser: require('@typescript-eslint/parser'),
parserOptions: {
ecmaFeatures: {},
},
},
plugins: {
'@angular-eslint': angularEslint.tsPlugin,
'@typescript-eslint': require('@typescript-eslint/eslint-plugin'),
import: require('eslint-plugin-import'),
promise: require('eslint-plugin-promise'),
'unused-imports': require('eslint-plugin-unused-imports'),
n: require('eslint-plugin-n'),
},
rules: {
// Your selectors
'@angular-eslint/directive-selector': [
'error',
{ type: 'attribute', prefix: 'app', style: 'camelCase' },
],
'@angular-eslint/component-selector': [
'error',
{ type: 'element', prefix: 'app', style: 'kebab-case' },
],
// Practical TS hygiene
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
'@typescript-eslint/consistent-type-imports': [
'warn',
{ prefer: 'type-imports', fixStyle: 'separate-type-imports' },
],
/* Base */
'no-debugger': 'warn',
/* TypeScript */
'unused-imports/no-unused-imports': 'warn',
'unused-imports/no-unused-vars': [
'warn',
{ vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_' },
],
'@typescript-eslint/no-explicit-any': 'error',
/* Imports */
'import/first': 'error',
'import/no-duplicates': 'error',
'import/newline-after-import': 'warn',
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: [
'**/*.config*.{js,cjs,mjs,ts}',
'**/scripts/**',
'**/*.test.{ts,js}',
'**/Testing.{ts,js}',
'**/.eslintrc.{js,cjs}',
],
},
],
/* Node */
'n/no-missing-import': 'off',
'n/no-unsupported-features/es-syntax': 'off',
/* Promis */
'promise/catch-or-return': 'warn',
'promise/always-return': 'off',
/* „Fire-and-forget“-Promises `void` */
'no-void': ['warn', { allowAsStatement: true }],
},
settings: {
'import/resolver': {
typescript: {
alwaysTryTypes: true,
},
node: true,
},
},
},
{
files: ['**/*.{js,cjs}'],
languageOptions: {
sourceType: 'script',
},
rules: {
'n/global-require': 'off',
},
},
];
module.exports = config;
+25
View File
@@ -0,0 +1,25 @@
/** @type {import('eslint').Linter.FlatConfig[]} */
const base = require('./eslint.config.js');
const typeAwareLayer = {
files: ['**/*.{ts,tsx}'],
languageOptions: {
parser: require('@typescript-eslint/parser'),
parserOptions: {
project: ['./tsconfig.eslint.json', './tsconfig.spec.json', './backend/tsconfig.json'],
tsconfigRootDir: __dirname,
},
},
plugins: {
'@typescript-eslint': require('@typescript-eslint/eslint-plugin'),
},
rules: {
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-misused-promises': ['warn', { checksVoidReturn: false }],
'@typescript-eslint/no-unsafe-assignment': 'warn',
'@typescript-eslint/no-unsafe-member-access': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
},
};
module.exports = [...base, typeAwareLayer];
+8743 -3985
View File
File diff suppressed because it is too large Load Diff
+76 -30
View File
@@ -1,65 +1,111 @@
{
"name": "angular-portfolio-seite",
"version": "3.1.11",
"main": "src/server.ts",
"name": "portfolio",
"version": "YYDDDhhmm+GIT_HASH",
"currentVersion": "3.2.0",
"engines": {
"node": ">=24.14.0",
"npm": ">=11.12.0"
},
"scripts": {
"dev": "npm run updateProjectVersion && ng serve",
"------ UTILS": "",
"format": "prettier --write .",
"format:check": "prettier --check .",
"lint": "npm run lint:base && npm run lint:types",
"lint:fix": "eslint . --ext .ts,.tsx,.js,.cjs,.mjs --fix",
"lint:base": "eslint . --ext .ts,.tsx,.js,.cjs,.mjs",
"lint:types": "eslint . --config eslint.config.typeaware.cjs --ext .ts,.tsx",
"text:check": "node scripts/check-source-quality.cjs",
"text:fix": "node scripts/fix-source-text.cjs --write",
"version:set": "node ./scripts/version.cjs --set",
"version:ensure": "node ./scripts/version.cjs --ensure",
"version:check": "node ./scripts/version.cjs --check",
"version:place": "node ./scripts/version.cjs --place",
"------ CI": "",
"check": "npm run build && npm run lint && npm run format:check && npm run version:check && npm run text:check",
"fix": "npm run version:place && npm run lint:fix && npm run text:fix && npm run format",
"------ BUILD": "",
"generate:static-files": "node scripts/generate-static-files.cjs",
"generate:assets": "npm run generate:static-files",
"build": "npm run generate:assets && ng build --configuration production && node scripts/copy-static-root-files.cjs",
"------ STARTUP": "",
"test": "ng test",
"clean": "del-cli dist && echo SUCCEED CLEAN",
"updatePackageVersions": "ng update @angular/cli @angular/core && ng update && npm update",
"updateProjectVersion": "npm version patch --no-git-tag-version && echo SUCCEED UPDATE VERSION",
"buildDevelopment": "npm run clean && ng build --configuration development && echo SUCCEED DEV BUILD && npm run updateProjectVersion",
"buildProduction": "npm run clean && ng build --configuration production && echo SUCCEED PROD BUILD && npm run updateProjectVersion",
"start": "node dist/server/server.mjs"
"dev": "npm run generate:assets && ng serve",
"start": "node dist/server/server.mjs",
"------ CONFIGS": "",
"prepare": "husky"
},
"lint-staged": {
"*.{ts,js}": [
"prettier --write",
"eslint --fix"
],
"*.{html,css,scss,json,md}": [
"prettier --write"
]
},
"repository": "https://github.com/FrauJulian/Personal-Portfolio-Website",
"bugs": "https://github.com/FrauJulian/Personal-Portfolio-Website/issues",
"author": {
"name": "Lechner Julian",
"nickname": "FrauJulian",
"email": "contact@fraujulian.xyz",
"email": "fraujulian@lechner.top",
"website": "https://fraujulian.xyz/"
},
"private": true,
"dependencies": {
"@angular/animations": "^19.2.14",
"@angular/cdk": "^19.2.19",
"@angular/common": "^19.2.0",
"@angular/compiler": "^19.2.0",
"@angular/core": "^19.2.0",
"@angular/forms": "^19.2.0",
"@angular/material": "^19.2.19",
"@angular/platform-browser": "^19.2.0",
"@angular/platform-browser-dynamic": "^19.2.0",
"@angular/platform-server": "^19.2.0",
"@angular/router": "^19.2.0",
"@angular/ssr": "^19.2.9",
"@fortawesome/angular-fontawesome": "^1.0.0",
"@angular/animations": "^21.2.9",
"@angular/cdk": "^21.2.7",
"@angular/common": "^21.2.9",
"@angular/compiler": "^21.2.9",
"@angular/core": "^21.2.9",
"@angular/forms": "^21.2.9",
"@angular/material": "^21.2.7",
"@angular/platform-browser": "^21.2.9",
"@angular/platform-browser-dynamic": "^21.2.9",
"@angular/platform-server": "^21.2.9",
"@angular/router": "^21.2.9",
"@angular/ssr": "^21.2.7",
"@fortawesome/angular-fontawesome": "^4.0.0",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@types/markdown-it": "^14.1.2",
"express": "^4.18.2",
"markdown-it": "^14.1.0",
"markdown-it": "^14.1.1",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^19.2.9",
"@angular/cli": "^19.2.9",
"@angular/compiler-cli": "^19.2.0",
"@angular-devkit/build-angular": "^21.2.7",
"@angular/cli": "^21.2.7",
"@angular/compiler-cli": "^21.2.9",
"@types/express": "^4.17.17",
"@types/jasmine": "~5.1.0",
"@types/markdown-it": "^14.1.2",
"@types/node": "^18.18.0",
"@types/showdown": "^2.0.6",
"@typescript-eslint/eslint-plugin": "8.57.2",
"@typescript-eslint/parser": "8.57.2",
"angular-eslint": "21.3.1",
"del-cli": "^6.0.0",
"esbuild": "^0.27.4",
"eslint": "9.39.4",
"eslint-import-resolver-typescript": "4.4.4",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-n": "17.24.0",
"eslint-plugin-promise": "7.2.1",
"eslint-plugin-unused-imports": "4.4.1",
"husky": "^9.1.7",
"jasmine-core": "~5.6.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.7.2"
"postcss": "^8.5.8",
"postcss-selector-parser": "^7.1.1",
"prettier": "^3.8.1",
"typescript": "~5.9.3",
"typescript-eslint": "8.57.2"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 873 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env node
const fs = require('node:fs');
const path = require('node:path');
const {
PROJECT_ROOT,
DEFAULT_ALLOWED_EXTENSIONS,
collectSourceFiles,
toProjectRelativePath,
} = require('./source-files.cjs');
const SOURCE_DIRECTORIES = [
path.join(PROJECT_ROOT, 'backend', 'src'),
path.join(PROJECT_ROOT, 'src'),
path.join(PROJECT_ROOT, 'scripts'),
];
const IGNORED_RELATIVE_PATHS = new Set([
'scripts/check-source-quality.cjs',
'scripts/fix-source-text.cjs',
]);
const ALLOWED_EXTENSIONS = DEFAULT_ALLOWED_EXTENSIONS;
const MOJIBAKE_PATTERNS = [
{ label: 'UTF8_AS_LATIN1', regex: /\u00C3[\u0080-\u00BF]/u },
{ label: 'REPLACEMENT_CHAR', regex: /\uFFFD/u },
{ label: 'MISDECODED_QUESTION_MARK', regex: /\u00EF\u00BF\u00BD/u },
{ label: 'CP1252_ARTIFACT', regex: /\u00C2[\u0080-\u00BF]?/u },
{
label: 'DOUBLE_DECODED_APOSTROPHE',
regex: /\u00E2\u20AC\u2122|\u00E2\u20AC\u0153|\u00E2\u20AC\u009D/u,
},
];
const ASCII_TRANSLITERATION_PATTERNS = [
{
label: 'ASCII_TRANSLITERATION',
regex:
/\b(?:fuer|Fuer|gueltig|Gueltig|ungueltig|Ungueltig|zurueck|Zurueck|ueberein|Ueberein|waehlen|Waehlen|zaehler|Zaehler|ausfuellen|Ausfuellen|geschuetzt|Geschuetzt|verschluesselt|Verschluesselt)\b/u,
},
];
const VAR_PATTERN = /\bvar\s+[A-Za-z_$][A-Za-z0-9_$]*/u;
/** @type {Array<{file: string, rule: string, detail: string}>} */
const issues = [];
for (const directoryPath of SOURCE_DIRECTORIES) {
const files = collectSourceFiles(directoryPath, ALLOWED_EXTENSIONS);
for (const filePath of files) {
const source = fs.readFileSync(filePath, 'utf8');
const relativePath = toProjectRelativePath(filePath);
const extension = path.extname(filePath).toLowerCase();
if (IGNORED_RELATIVE_PATHS.has(relativePath)) {
continue;
}
for (const pattern of MOJIBAKE_PATTERNS) {
if (pattern.regex.test(source)) {
issues.push({
file: relativePath,
rule: pattern.label,
detail: 'possible mojibake detected',
});
}
}
for (const pattern of ASCII_TRANSLITERATION_PATTERNS) {
if (pattern.regex.test(source)) {
issues.push({
file: relativePath,
rule: pattern.label,
detail: 'possible ASCII transliteration detected',
});
}
}
if (extension !== '.html' && VAR_PATTERN.test(source)) {
issues.push({
file: relativePath,
rule: 'NO_VAR',
detail: 'use const/let instead of var',
});
}
}
}
if (issues.length === 0) {
console.log('Source quality check passed: no mojibake artifacts and no var declarations found.');
process.exit(0);
}
console.error('Source quality check failed:');
for (const issue of issues) {
console.error(`- ${issue.file} [${issue.rule}] ${issue.detail}`);
}
process.exit(1);
+40
View File
@@ -0,0 +1,40 @@
const fs = require('fs');
const path = require('path');
const sources = [
{
source: path.join(process.cwd(), 'src', 'robots.txt'),
fileName: 'robots.txt',
},
{
source: path.join(process.cwd(), 'src', 'sitemap.xml'),
fileName: 'sitemap.xml',
},
];
const distRoot = path.join(process.cwd(), 'dist');
const browserDir = path.join(distRoot, 'browser');
const targetDirectories = [distRoot, browserDir]
.filter((targetDirectory) => fs.existsSync(targetDirectory))
.filter((targetDirectory, index, list) => list.indexOf(targetDirectory) === index);
if (targetDirectories.length === 0) {
console.warn('Skipping static root file copy: dist directory not found.');
process.exit(0);
}
for (const { source, fileName } of sources) {
if (!fs.existsSync(source)) {
console.warn(`Skipping ${fileName} copy: source file not found.`);
continue;
}
for (const targetDirectory of targetDirectories) {
const targetFile = path.join(targetDirectory, fileName);
fs.copyFileSync(source, targetFile);
console.log(
`Copied ${path.relative(process.cwd(), source)} -> ${path.relative(process.cwd(), targetFile)}`,
);
}
}
+107
View File
@@ -0,0 +1,107 @@
const fs = require('fs');
const path = require('path');
const ENV_FILE_CANDIDATES = [
path.join(process.cwd(), '.env'),
path.join(process.cwd(), 'backend', '.env'),
];
function applyEnvFile(filePath, env = process.env) {
const rawFile = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/u, '');
const lines = rawFile.split(/\r?\n/u);
for (const line of lines) {
const trimmedLine = line.trim();
if (!trimmedLine || trimmedLine.startsWith('#')) {
continue;
}
const withoutExport = trimmedLine.startsWith('export ')
? trimmedLine.slice('export '.length).trim()
: trimmedLine;
const separatorIndex = withoutExport.indexOf('=');
if (separatorIndex <= 0) {
continue;
}
const key = withoutExport.slice(0, separatorIndex).trim();
let value = withoutExport.slice(separatorIndex + 1).trim();
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key) || key in env) {
continue;
}
if (value.startsWith('"') && value.endsWith('"')) {
value = value
.slice(1, -1)
.replace(/\\n/gu, '\n')
.replace(/\\r/gu, '\r')
.replace(/\\t/gu, '\t')
.replace(/\\"/gu, '"')
.replace(/\\\\/gu, '\\');
} else if (value.startsWith("'") && value.endsWith("'")) {
value = value.slice(1, -1);
} else {
value = value.replace(/\s+#.*$/u, '').trim();
}
env[key] = value;
}
}
function loadEnvFromFileCandidates(envFileCandidates = ENV_FILE_CANDIDATES, env = process.env) {
for (const envFilePath of envFileCandidates) {
if (!fs.existsSync(envFilePath)) {
continue;
}
applyEnvFile(envFilePath, env);
return envFilePath;
}
return null;
}
function parsePositiveIntEnv(names, fallback, env = process.env) {
const candidates = Array.isArray(names) ? names : [names];
for (const name of candidates) {
const rawValue = env[name];
if (rawValue === undefined || rawValue === null || rawValue.trim().length === 0) {
continue;
}
const parsed = Number.parseInt(rawValue, 10);
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new Error(`${name} must be a positive integer. Received "${rawValue}".`);
}
return parsed;
}
return fallback;
}
function parseTimeZoneEnv(name, fallback, env = process.env) {
const rawValue = env[name];
const value =
rawValue === undefined || rawValue === null || rawValue.trim().length === 0
? fallback
: rawValue.trim();
try {
new Intl.DateTimeFormat('en-GB', {
timeZone: value,
});
} catch {
throw new Error(`${name} must be a valid IANA timezone. Received "${rawValue}".`);
}
return value;
}
module.exports = {
loadEnvFromFileCandidates,
parsePositiveIntEnv,
parseTimeZoneEnv,
};
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env node
const fs = require('node:fs');
const path = require('node:path');
const {
PROJECT_ROOT,
DEFAULT_ALLOWED_EXTENSIONS,
collectSourceFiles,
toProjectRelativePath,
} = require('./source-files.cjs');
const SOURCE_DIRECTORIES = [
path.join(PROJECT_ROOT, 'backend', 'src'),
path.join(PROJECT_ROOT, 'src'),
];
const ALLOWED_EXTENSIONS = DEFAULT_ALLOWED_EXTENSIONS;
const WRITE_MODE = process.argv.includes('--write');
const DIRECT_REPLACEMENTS = [
['Ä', 'Ä'],
['Ö', 'Ö'],
['Ü', 'Ü'],
['ä', 'ä'],
['ö', 'ö'],
['ü', 'ü'],
['ß', 'ß'],
['’', ''],
['“', '“'],
['”', '”'],
['–', ''],
['—', '—'],
['…', '…'],
['§', '§'],
['&Auml;', 'Ä'],
['&Ouml;', 'Ö'],
['&Uuml;', 'Ü'],
['&auml;', 'ä'],
['&ouml;', 'ö'],
['&uuml;', 'ü'],
['&szlig;', 'ß'],
['&sect;', '§'],
];
const WORD_REPLACEMENTS = [
[/\bAufrufzaehler\b/gu, 'Aufrufzähler'],
[/\baufzaehlen\b/gu, 'aufzählen'],
[/\bAusfuellen\b/gu, 'Ausfüllen'],
[/\bausfuellen\b/gu, 'ausfüllen'],
[/\bAuswaehlen\b/gu, 'Auswählen'],
[/\bauswaehlen\b/gu, 'auswählen'],
[/\bEnthaelt\b/gu, 'Enthält'],
[/\benthaelt\b/gu, 'enthält'],
[/\bFuer\b/gu, 'Für'],
[/\bfuer\b/gu, 'für'],
[/\bGeschuetzt\b/gu, 'Geschützt'],
[/\bgeschuetzt\b/gu, 'geschützt'],
[/\bGueltig\b/gu, 'Gültig'],
[/\bgueltig\b/gu, 'gültig'],
[/\bOesterreich\b/gu, 'Österreich'],
[/\boesterreich\b/gu, 'österreich'],
[/\bUeberein\b/gu, 'Überein'],
[/\bueberein\b/gu, 'überein'],
[/\bUngueltig\b/gu, 'Ungültig'],
[/\bungueltig\b/gu, 'ungültig'],
[/\bVerschluesselt\b/gu, 'Verschlüsselt'],
[/\bverschluesselt\b/gu, 'verschlüsselt'],
[/\bWaehlen\b/gu, 'Wählen'],
[/\bwaehlen\b/gu, 'wählen'],
[/\bZaehler\b/gu, 'Zähler'],
[/\bzaehler\b/gu, 'zähler'],
[/\bZurueck\b/gu, 'Zurück'],
[/\bzurueck\b/gu, 'zurück'],
];
function applyTextFixes(source) {
let nextSource = source;
for (const [searchValue, replaceValue] of DIRECT_REPLACEMENTS) {
if (nextSource.includes(searchValue)) {
nextSource = nextSource.split(searchValue).join(replaceValue);
}
}
for (const [pattern, replaceValue] of WORD_REPLACEMENTS) {
nextSource = nextSource.replace(pattern, replaceValue);
}
return nextSource;
}
const changedFiles = [];
for (const directoryPath of SOURCE_DIRECTORIES) {
for (const filePath of collectSourceFiles(directoryPath, ALLOWED_EXTENSIONS)) {
const source = fs.readFileSync(filePath, 'utf8');
const nextSource = applyTextFixes(source);
if (nextSource === source) {
continue;
}
changedFiles.push(toProjectRelativePath(filePath));
if (WRITE_MODE) {
fs.writeFileSync(filePath, nextSource, 'utf8');
}
}
}
if (changedFiles.length === 0) {
console.log(`Source text fixer: no changes ${WRITE_MODE ? 'written' : 'needed'}.`);
process.exit(0);
}
const actionLabel = WRITE_MODE ? 'updated' : 'would update';
console.log(`Source text fixer ${actionLabel} ${changedFiles.length} file(s):`);
for (const filePath of changedFiles) {
console.log(`- ${filePath}`);
}
process.exit(WRITE_MODE ? 0 : 1);
+117
View File
@@ -0,0 +1,117 @@
const fs = require('fs');
const path = require('path');
const { loadEnvFromFileCandidates } = require('./env-utils.cjs');
const staticSiteConfig = require('./static-site.config.cjs');
loadEnvFromFileCandidates();
const outputDir = path.join(process.cwd(), 'src');
const robotsPath = path.join(outputDir, 'robots.txt');
const sitemapPath = path.join(outputDir, 'sitemap.xml');
const configuredSiteUrl = process.env.PUBLIC_SITE_URL || staticSiteConfig.siteUrl;
const siteUrl = normalizeSiteUrl(configuredSiteUrl);
if (!siteUrl) {
throw new Error(
'Missing site URL for static file generation. Set PUBLIC_SITE_URL or scripts/static-site.config.cjs.',
);
}
const robotsContent = createRobotsContent(staticSiteConfig.robots, siteUrl);
const sitemapContent = createSitemapContent(staticSiteConfig.sitemap.routes, siteUrl);
fs.writeFileSync(robotsPath, `${robotsContent}\n`, 'utf8');
fs.writeFileSync(sitemapPath, `${sitemapContent}\n`, 'utf8');
console.log(`Generated ${path.relative(process.cwd(), robotsPath)}`);
console.log(`Generated ${path.relative(process.cwd(), sitemapPath)}`);
function normalizeSiteUrl(rawSiteUrl) {
if (typeof rawSiteUrl !== 'string') {
return '';
}
const trimmedSiteUrl = rawSiteUrl.trim().replace(/\/+$/u, '');
if (!trimmedSiteUrl) {
return '';
}
try {
const normalizedUrl = new URL(trimmedSiteUrl);
return normalizedUrl.toString().replace(/\/+$/u, '');
} catch {
throw new Error(`Invalid site URL "${rawSiteUrl}" in static site configuration.`);
}
}
function createRobotsContent(robotsConfig, resolvedSiteUrl) {
const lines = ['User-agent: *', ''];
for (const allowedPath of robotsConfig.allow) {
lines.push(`Allow: ${allowedPath}`);
}
for (const disallowedPath of robotsConfig.disallow) {
lines.push(`Disallow: ${disallowedPath}`);
}
lines.push('', `Sitemap: ${resolvedSiteUrl}/sitemap.xml`);
return lines.join('\n');
}
function createSitemapContent(routes, resolvedSiteUrl) {
const entries = routes
.map((route) => {
const normalizedPath = normalizeRoutePath(route.path);
const parts = [
' <url>',
` <loc>${escapeXml(`${resolvedSiteUrl}${normalizedPath}`)}</loc>`,
];
if (route.lastModified) {
parts.push(` <lastmod>${escapeXml(route.lastModified)}</lastmod>`);
}
if (route.changeFrequency) {
parts.push(` <changefreq>${escapeXml(route.changeFrequency)}</changefreq>`);
}
if (route.priority) {
parts.push(` <priority>${escapeXml(route.priority)}</priority>`);
}
parts.push(' </url>');
return parts.join('\n');
})
.join('\n');
return [
'<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
entries,
'</urlset>',
].join('\n');
}
function normalizeRoutePath(routePath) {
if (routePath === '/') {
return '';
}
if (typeof routePath !== 'string' || !routePath.startsWith('/')) {
throw new Error(`Invalid sitemap route "${routePath}". Routes must start with "/".`);
}
return routePath;
}
function escapeXml(value) {
return value
.replace(/&/gu, '&amp;')
.replace(/"/gu, '&quot;')
.replace(/'/gu, '&apos;')
.replace(/</gu, '&lt;')
.replace(/>/gu, '&gt;');
}
+63
View File
@@ -0,0 +1,63 @@
const fs = require('node:fs');
const path = require('node:path');
const PROJECT_ROOT = path.resolve(__dirname, '..');
const DEFAULT_ALLOWED_EXTENSIONS = new Set(['.ts', '.js', '.cjs', '.html', '.css']);
/**
* @param {string} directoryPath
* @param {Set<string>} [allowedExtensions]
* @returns {string[]}
*/
function collectSourceFiles(directoryPath, allowedExtensions = DEFAULT_ALLOWED_EXTENSIONS) {
/** @type {string[]} */
const results = [];
if (!fs.existsSync(directoryPath)) {
return results;
}
/** @type {string[]} */
const queue = [directoryPath];
while (queue.length > 0) {
const currentPath = queue.pop();
if (!currentPath) {
continue;
}
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
for (const entry of entries) {
const absolutePath = path.join(currentPath, entry.name);
if (entry.isDirectory()) {
queue.push(absolutePath);
continue;
}
if (!entry.isFile()) {
continue;
}
const extension = path.extname(entry.name).toLowerCase();
if (allowedExtensions.has(extension)) {
results.push(absolutePath);
}
}
}
return results;
}
/**
* @param {string} absolutePath
* @returns {string}
*/
function toProjectRelativePath(absolutePath) {
return path.relative(PROJECT_ROOT, absolutePath).split(path.sep).join('/');
}
module.exports = {
PROJECT_ROOT,
DEFAULT_ALLOWED_EXTENSIONS,
collectSourceFiles,
toProjectRelativePath,
};
+17
View File
@@ -0,0 +1,17 @@
module.exports = {
siteUrl: 'https://fraujulian.xyz',
sitemap: {
routes: [
{
path: '/',
lastModified: '2026-04-17',
changeFrequency: 'monthly',
priority: '1.0',
},
],
},
robots: {
allow: ['/'],
disallow: [],
},
};
+199
View File
@@ -0,0 +1,199 @@
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const { execFileSync } = require('node:child_process');
const PROJECT_ROOT = path.resolve(__dirname, '..');
const PACKAGE_JSON_PATH = path.join(PROJECT_ROOT, 'package.json');
const PACKAGE_LOCK_PATH = path.join(PROJECT_ROOT, 'package-lock.json');
const VERSION_PLACEHOLDER = 'YYDDDhhmm+GIT_HASH';
const RESOLVED_BUILD_PATTERN = /^\d{2}\d{3}\d{4}\+[A-Za-z0-9_-]+$/;
const MODES = new Set(['--set', '--ensure', '--check', '--place']);
function readJson(location) {
return JSON.parse(fs.readFileSync(location, 'utf8'));
}
function writeJson(location, value) {
fs.writeFileSync(location, `${JSON.stringify(value, null, 4)}\n`);
}
function getSelectedMode() {
let selectedModes = process.argv.slice(2).filter((value) => MODES.has(value));
if (selectedModes.length !== 1) {
throw new Error('Use exactly one mode: --set, --ensure, --check or --place.');
}
return selectedModes[0];
}
function getVersionParts(version) {
let parts = String(version).split('.');
if (parts.length < 3) {
throw new Error(`Version "${version}" does not follow the expected major.minor.build format.`);
}
return parts;
}
function getVersionPrefix(version) {
let [major, minor] = getVersionParts(version);
return `${major}.${minor}.`;
}
function getBuildPart(version) {
let [, , buildPart] = getVersionParts(version);
return buildPart;
}
function hasPlaceholderVersion(version) {
return version === VERSION_PLACEHOLDER;
}
function hasResolvedVersion(version) {
let buildPart = getBuildPart(version);
return buildPart !== VERSION_PLACEHOLDER && RESOLVED_BUILD_PATTERN.test(buildPart);
}
function getExplicitVersion() {
return process.env.BUILD_VERSION?.trim() || '';
}
function getGitHash() {
let explicitHash = process.env.BUILD_GIT_HASH?.trim() || process.env.GITHUB_SHA?.trim();
if (explicitHash) {
return explicitHash.slice(0, 7);
}
try {
return execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
cwd: PROJECT_ROOT,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
} catch {
return 'nogit';
}
}
function getBuildNumber() {
let now = new Date();
let year = String(now.getFullYear()).slice(-2);
let startOfYear = new Date(now.getFullYear(), 0, 1);
let dayOfYear = Math.floor((now - startOfYear) / (1000 * 60 * 60 * 24)) + 1;
let hours = String(now.getHours()).padStart(2, '0');
let minutes = String(now.getMinutes()).padStart(2, '0');
return `${year}${String(dayOfYear).padStart(3, '0')}${hours}${minutes}`;
}
function resolveVersion(currentVersion) {
let explicitVersion = getExplicitVersion();
if (explicitVersion) {
return explicitVersion;
}
return `${getVersionPrefix(currentVersion)}${getBuildNumber()}-${getGitHash()}`;
}
function isAlreadyEnsured(currentVersion) {
let explicitVersion = getExplicitVersion();
if (explicitVersion) {
return currentVersion === explicitVersion;
}
return hasResolvedVersion(currentVersion);
}
function writeResolvedVersion(resolvedVersion) {
let packageJson = readJson(PACKAGE_JSON_PATH);
packageJson.version = resolvedVersion;
writeJson(PACKAGE_JSON_PATH, packageJson);
if (!fs.existsSync(PACKAGE_LOCK_PATH)) {
return;
}
let packageLock = readJson(PACKAGE_LOCK_PATH);
packageLock.version = resolvedVersion;
if (packageLock.packages && packageLock.packages['']) {
packageLock.packages[''].version = resolvedVersion;
}
writeJson(PACKAGE_LOCK_PATH, packageLock);
}
function runCheck(currentVersion) {
if (hasPlaceholderVersion(currentVersion)) {
console.log(`Placeholder version detected: ${currentVersion}`);
return;
}
throw new Error(
`Resolved build version detected: ${currentVersion}\nShould be: ${VERSION_PLACEHOLDER}`,
);
}
function runEnsure(currentVersion) {
if (isAlreadyEnsured(currentVersion)) {
console.log(`Version already set: ${currentVersion}`);
return;
}
if (hasPlaceholderVersion(currentVersion)) {
throw new Error(`Placeholder version detected: ${currentVersion}`);
}
if (getExplicitVersion()) {
throw new Error(
`Version "${currentVersion}" does not match BUILD_VERSION "${getExplicitVersion()}".`,
);
}
throw new Error(`Version "${currentVersion}" is not a resolved build version.`);
}
function runSet(currentVersion) {
let resolvedVersion = resolveVersion(currentVersion);
writeResolvedVersion(resolvedVersion);
console.log(`Version set to: ${resolvedVersion}`);
}
function runSetPlace() {
writeResolvedVersion(VERSION_PLACEHOLDER);
console.log(`Version set to: ${VERSION_PLACEHOLDER}`);
}
try {
let selectedMode = getSelectedMode();
let packageJson = readJson(PACKAGE_JSON_PATH);
let packageVersion = String(packageJson.version);
let currentVersion = String(packageJson.currentVersion);
switch (selectedMode) {
case '--check':
runCheck(packageVersion);
break;
case '--ensure':
runEnsure(packageVersion);
break;
case '--set':
runSet(currentVersion);
break;
case '--place':
runSetPlace();
break;
default:
throw new Error(`Unknown mode: ${selectedMode}`);
}
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
+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);
+13 -11
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({
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(
routes,
withInMemoryScrolling({
anchorScrolling: 'enabled',
scrollPositionRestoration: 'top'
})),
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();
}
+134 -124
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>
</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>
<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>
<hr>
<br>
<div class="bio grid-container">
<div class="grid-item-start">
<h1>I work with</h1>
</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>
<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>
<a href="tel:{{globalFields.hrefContactTel}}">
<fa-icon class="fab" [icon]="faPhone" size="3x"></fa-icon>
<a href="tel:{{ global.hrefContactPhone }}" aria-label="Call phone number">
<fa-icon class="fab" [icon]="faPhone" size="lg"></fa-icon>
</a>
<a href="https://discord.com/users/860206216893693973">
<fa-icon class="fab" [icon]="faDiscord" size="3x"></fa-icon>
<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">
<fa-icon class="fab" [icon]="faGithub" size="3x"></fa-icon>
<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.xing.com/profile/Julian_Lechner03274">
<fa-icon class="fab" [icon]="faXing" size="3x"></fa-icon>
<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>
<a href="https://www.linkedin.com/in/julian-lechner-98b377356/">
<fa-icon class="fab" [icon]="faLinkedin" size="3x"></fa-icon>
</a>
<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>
</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 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>
</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 {
+79 -70
View File
@@ -1,76 +1,85 @@
<div class="container">
<div class="card profile">
<h1>Imprint</h1>
<h2>Information according to §5 ECG</h2>
{{ globalFields.firstname }} {{ globalFields.lastname }} <br>
{{ street }} {{ houseNumber }} <br>
{{ zip }} {{ city }} <br>
{{ country }} <br>
<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>
<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>
<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>
<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>
<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>
<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 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>
<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');
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

+137 -8
View File
@@ -1,9 +1,138 @@
export class globalFields {
public static readonly firstname: string = 'Julian';
public static readonly lastname: string = 'Lechner';
public static readonly contactMail: string = 'office@lechner-systems.at';
public static readonly contactPhone: string = '+43 (0) 660 9254001';
public static readonly hrefContactTel: string = '+436609254001';
public static readonly githubUsername: string = 'FrauJulian';
public static readonly numberOfLoadedRepositories: number = 999;
export interface PortfolioProject {
title: string;
link: string;
description: string;
languages: string[];
icon?: string;
CircleIcon?: boolean;
}
export interface PortraitHighlight {
image: string;
text: string;
}
export interface ImprintData {
street: string;
houseNumber: number;
zip: number;
city: string;
country: string;
}
export interface Global {
firstname: string;
lastname: string;
contactMail: string;
abuseMail: string;
contactPhone: string;
hrefContactPhone: string;
birthdate: string;
bioTextsList: string[];
portraitHighlights: PortraitHighlight[];
projects: PortfolioProject[];
address: ImprintData;
}
export const global: Global = {
firstname: 'Julian',
lastname: 'Lechner',
contactMail: 'fraujulian@lechner.top',
abuseMail: 'abuse@lechner-systems.at',
contactPhone: '+43 (0) 660 9254001',
hrefContactPhone: '+436609254001',
birthdate: '2009-03-03',
bioTextsList: [
'C#',
'.NET',
'.NET Framework',
'WPF',
'Avalonia',
'NodeJS',
'Angular',
'TypeScript',
'JavaScript',
'Java',
'HTML',
'(S)CSS',
'Github',
'Gitlab',
'Azure DevOps',
'Ubuntu',
'Debian',
'SSMS',
'MariaDB',
'MySQL',
'Grandle',
],
portraitHighlights: [
{
image: 'assets/portrait/me.png',
text: 'ME',
},
{
image: 'assets/portrait/love.jpg',
text: 'My Love',
},
{
image: 'assets/portrait/anniversary.jpg',
text: 'Anniversary',
},
{
image: 'assets/portrait/trains.jpg',
text: 'Trains',
},
{
image: 'assets/portrait/traveling.jpg',
text: 'Traveling',
},
{
image: 'assets/portrait/culture.jpg',
text: 'Culture',
},
],
projects: [
{
title: 'SynRadio',
link: 'https://www.synradio.de/',
description: 'An internet radio station that is available on various media.',
languages: ['TypeScript', 'JavaScript', 'HTML', 'CSS'],
icon: 'https://gerlach-systems.de/IMAGES/SYNHOST/SYNHOST_DARK.png',
CircleIcon: false,
},
{
title: 'SynHost',
link: 'https://www.synhost.de/',
description:
'A support system integrating various platforms and media to offer employees a unified overview.',
languages: ['TypeScript'],
icon: 'https://gerlach-systems.de/IMAGES/SYNHOST/SYNHOST_DARK.png',
CircleIcon: false,
},
{
title: 'SynHost',
link: 'https://www.synhost.de/',
description:
'A support system integrating various platforms and media to offer employees a unified overview.',
languages: ['TypeScript'],
icon: 'https://gerlach-systems.de/IMAGES/SYNHOST/SYNHOST_DARK.png',
CircleIcon: false,
},
{
title: 'SynHost',
link: 'https://www.synhost.de/',
description:
'A support system integrating various platforms and media to offer employees a unified overview.',
languages: ['TypeScript'],
icon: 'https://gerlach-systems.de/IMAGES/SYNHOST/SYNHOST_DARK.png',
CircleIcon: false,
},
],
address: {
street: 'Ulmenstraße',
houseNumber: 9,
zip: 3380,
city: 'Pöchlarn',
country: 'Austria',
},
};
+9 -19
View File
@@ -1,24 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8">
<base href="/">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset="utf-8" />
<base href="/" />
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<title>Lechner Julian</title>
</head>
<body>
<app-root></app-root>
<script src='https://storage.ko-fi.com/cdn/scripts/overlay-widget.js'></script>
<script>
kofiWidgetOverlay.draw('fraujulian', {
'type': 'floating-chat',
'floating-chat.donateButton.text': 'Support me',
'floating-chat.donateButton.background-color': '#5bc0de',
'floating-chat.donateButton.text-color': '#323842'
});
</script>
</body>
</head>
<body>
<app-root></app-root>
</body>
</html>
+14 -5
View File
@@ -1,9 +1,18 @@
import {bootstrapApplication} from '@angular/platform-browser';
import {ApplicationRef} from '@angular/core';
import type { BootstrapContext } from '@angular/platform-browser';
import { bootstrapApplication } from '@angular/platform-browser';
import type { ApplicationRef } from '@angular/core';
import { provideZoneChangeDetection } from '@angular/core';
import {AppComponent} from './app/app.component';
import {config} from './app/app.config.server';
import { AppComponent } from './app/app.component';
import { config } from './app/app.config.server';
const bootstrap: () => Promise<ApplicationRef> = (): Promise<ApplicationRef> => bootstrapApplication(AppComponent, config);
const bootstrap: (context: BootstrapContext) => Promise<ApplicationRef> = (
context: BootstrapContext,
): Promise<ApplicationRef> =>
bootstrapApplication(
AppComponent,
{ ...config, providers: [provideZoneChangeDetection(), ...config.providers] },
context,
);
export default bootstrap;
+4 -5
View File
@@ -1,7 +1,6 @@
import {bootstrapApplication} from '@angular/platform-browser';
import { bootstrapApplication } from '@angular/platform-browser';
import {appConfig} from './app/app.config';
import {AppComponent} from './app/app.component';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig)
.catch((err: Error): void => console.error(err.stack));
bootstrapApplication(AppComponent, appConfig).catch((err: Error): void => console.error(err.stack));
+5
View File
@@ -0,0 +1,5 @@
User-agent: *
Allow: /
Sitemap: https://fraujulian.xyz/sitemap.xml
+9 -8
View File
@@ -1,8 +1,9 @@
import {APP_BASE_HREF} from '@angular/common';
import {CommonEngine, isMainModule} from '@angular/ssr/node';
import express, {Express, NextFunction, Request, Response} from 'express';
import {dirname, join, resolve} from 'node:path';
import {fileURLToPath} from 'node:url';
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine, isMainModule } from '@angular/ssr/node';
import type { Express, NextFunction, Request, Response } from 'express';
import express from 'express';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import bootstrap from './main.server';
import ProductionConfig from '../configs/Production.json';
@@ -18,12 +19,12 @@ app.get(
'**',
express.static(browserDistFolder, {
maxAge: '1y',
index: 'index.html'
index: 'index.html',
}),
);
app.get('**', (req: Request, res: Response, next: NextFunction): void => {
const {protocol, originalUrl, baseUrl, headers} = req;
const { protocol, originalUrl, baseUrl, headers } = req;
commonEngine
.render({
@@ -31,7 +32,7 @@ app.get('**', (req: Request, res: Response, next: NextFunction): void => {
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: browserDistFolder,
providers: [{provide: APP_BASE_HREF, useValue: baseUrl}],
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
})
.then((html: string): Response => res.send(html))
.catch((err: Error): void => next(err.stack));
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://fraujulian.xyz</loc>
<lastmod>2026-04-17</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
</urlset>
+757 -465
View File
File diff suppressed because it is too large Load Diff
+3 -11
View File
@@ -4,16 +4,8 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": [
"node"
]
"types": ["node"]
},
"files": [
"src/main.ts",
"src/main.server.ts",
"src/server.ts"
],
"include": [
"src/**/*.d.ts"
]
"files": ["src/main.ts", "src/main.server.ts", "src/server.ts"],
"include": ["src/**/*.d.ts"]
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": []
},
"include": ["src/**/*.ts", "src/**/*.d.ts"],
"exclude": ["src/**/*.spec.ts", "src/test.ts"]
}
+2 -7
View File
@@ -4,12 +4,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
"types": ["jasmine"]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
"include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
}