feat: reworked portfolio
@@ -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.*
|
||||||
@@ -1,17 +1,9 @@
|
|||||||
# Editor configuration, see https://editorconfig.org
|
|
||||||
root = true
|
root = true
|
||||||
|
|
||||||
[*]
|
[*]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
|
end_of_line = crlf
|
||||||
|
insert_final_newline = true
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
insert_final_newline = true
|
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
[*.ts]
|
|
||||||
quote_type = single
|
|
||||||
ij_typescript_use_double_quotes = false
|
|
||||||
|
|
||||||
[*.md]
|
|
||||||
max_line_length = off
|
|
||||||
trim_trailing_whitespace = false
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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'
|
||||||
@@ -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 }}
|
|
||||||
@@ -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 }}
|
||||||
@@ -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
|
name: Build Validation
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, reopened]
|
types: [opened, synchronize, reopened]
|
||||||
|
|
||||||
@@ -11,16 +15,13 @@ jobs:
|
|||||||
- name: checkout Repository
|
- name: checkout Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: use NodeJS v22.16.0
|
- name: use NodeJS v24.14.0
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '22.16.0'
|
node-version: '24.14.0'
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: install Dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
- name: run Build DEV
|
- name: run Build Validation
|
||||||
run: npm run buildDevelopment
|
run: npm run check
|
||||||
|
|
||||||
- name: run Build PROD
|
|
||||||
run: npm run buildProduction
|
|
||||||
@@ -1,42 +1,38 @@
|
|||||||
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
# Paketmanager-Abhaengigkeiten
|
||||||
|
|
||||||
# Compiled output
|
|
||||||
/dist
|
|
||||||
/tmp
|
|
||||||
/out-tsc
|
|
||||||
/bazel-out
|
|
||||||
|
|
||||||
# Node
|
|
||||||
/node_modules
|
/node_modules
|
||||||
npm-debug.log
|
|
||||||
yarn-error.log
|
|
||||||
|
|
||||||
# IDEs and editors
|
# Lokale Tool-Caches
|
||||||
.idea/
|
.npm-cache
|
||||||
.project
|
.eslintcache
|
||||||
.classpath
|
|
||||||
.c9/
|
|
||||||
*.launch
|
|
||||||
.settings/
|
|
||||||
*.sublime-workspace
|
|
||||||
|
|
||||||
# Visual Studio Code
|
# Generierte Build- und Test-Ausgaben
|
||||||
.vscode/*
|
/dist
|
||||||
!.vscode/settings.json
|
|
||||||
!.vscode/tasks.json
|
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.history/*
|
|
||||||
|
|
||||||
# Miscellaneous
|
|
||||||
/.angular/cache
|
|
||||||
.sass-cache/
|
|
||||||
/connect.lock
|
|
||||||
/coverage
|
/coverage
|
||||||
/libpeerconnection.log
|
|
||||||
testem.log
|
|
||||||
/typings
|
|
||||||
|
|
||||||
# System files
|
# Betriebssystem-Dateileichen
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
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
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
npm run fix || exit 1
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
npm run check || exit 1
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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/`.
|
||||||
@@ -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"]
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
# 🖼️ FrauJulian's Portfolio Website
|
# 🖼️ FrauJulian's Portfolio Website
|
||||||
|
|
||||||
### [🙋♂️ Website Preview](https://fraujulian.xyz/)
|
### [🙋♂️ Website Preview](https://fraujulian.xyz/)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 👂 Languages/Framework/Packages:
|
## 👂 Languages/Framework/Packages:
|
||||||
|
|
||||||
- Angular v19
|
- Angular v19
|
||||||
- TypeScript
|
- TypeScript
|
||||||
- SCSS
|
- SCSS
|
||||||
@@ -28,7 +30,9 @@ or for Production Build
|
|||||||
npm run buildProduction
|
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
|
### Start compiled Project
|
||||||
|
|
||||||
@@ -38,7 +42,8 @@ npm run start
|
|||||||
|
|
||||||
## Running unit tests
|
## 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
|
```bash
|
||||||
nom run test
|
nom run test
|
||||||
@@ -51,10 +56,12 @@ npm run updatePackageVersions
|
|||||||
```
|
```
|
||||||
|
|
||||||
## 📋 Credits:
|
## 📋 Credits:
|
||||||
|
|
||||||
~ made by [**FrauJulian**](https://fraujulian.xyz/).
|
~ made by [**FrauJulian**](https://fraujulian.xyz/).
|
||||||
|
|
||||||
## 🤝 Enjoy?
|
## 🤝 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! ⛰️
|
### Greetings from Austria! ⛰️
|
||||||
|
|||||||
@@ -20,21 +20,21 @@
|
|||||||
"outputPath": "dist",
|
"outputPath": "dist",
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
"browser": "src/main.ts",
|
"browser": "src/main.ts",
|
||||||
"polyfills": [
|
"polyfills": ["zone.js"],
|
||||||
"zone.js"
|
|
||||||
],
|
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
"inlineStyleLanguage": "scss",
|
"inlineStyleLanguage": "scss",
|
||||||
"assets": [
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "src/assets",
|
||||||
|
"output": "assets"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"glob": "**/*",
|
"glob": "**/*",
|
||||||
"input": "public"
|
"input": "public"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": ["node_modules/@angular/material/prebuilt-themes/rose-red.css", "src/styles.scss"],
|
||||||
"node_modules/@angular/material/prebuilt-themes/rose-red.css",
|
|
||||||
"src/styles.scss"
|
|
||||||
],
|
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"server": "src/main.server.ts",
|
"server": "src/main.server.ts",
|
||||||
"prerender": true,
|
"prerender": true,
|
||||||
@@ -99,23 +99,22 @@
|
|||||||
"test": {
|
"test": {
|
||||||
"builder": "@angular-devkit/build-angular:karma",
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
"options": {
|
"options": {
|
||||||
"polyfills": [
|
"polyfills": ["zone.js", "zone.js/testing"],
|
||||||
"zone.js",
|
|
||||||
"zone.js/testing"
|
|
||||||
],
|
|
||||||
"tsConfig": "tsconfig.spec.json",
|
"tsConfig": "tsconfig.spec.json",
|
||||||
"inlineStyleLanguage": "scss",
|
"inlineStyleLanguage": "scss",
|
||||||
"assets": [
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "src/assets",
|
||||||
|
"output": "assets"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"glob": "**/*",
|
"glob": "**/*",
|
||||||
"input": "public",
|
"input": "public",
|
||||||
"output": "public"
|
"output": "public"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": ["node_modules/@angular/material/prebuilt-themes/rose-red.css", "src/styles.scss"],
|
||||||
"node_modules/@angular/material/prebuilt-themes/rose-red.css",
|
|
||||||
"src/styles.scss"
|
|
||||||
],
|
|
||||||
"scripts": []
|
"scripts": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,5 +123,31 @@
|
|||||||
},
|
},
|
||||||
"cli": {
|
"cli": {
|
||||||
"analytics": false
|
"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": "."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -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;
|
||||||
@@ -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];
|
||||||
@@ -1,65 +1,111 @@
|
|||||||
{
|
{
|
||||||
"name": "angular-portfolio-seite",
|
"name": "portfolio",
|
||||||
"version": "3.1.11",
|
"version": "YYDDDhhmm+GIT_HASH",
|
||||||
"main": "src/server.ts",
|
"currentVersion": "3.2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24.14.0",
|
||||||
|
"npm": ">=11.12.0"
|
||||||
|
},
|
||||||
"scripts": {
|
"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",
|
"test": "ng test",
|
||||||
"clean": "del-cli dist && echo SUCCEED CLEAN",
|
"dev": "npm run generate:assets && ng serve",
|
||||||
"updatePackageVersions": "ng update @angular/cli @angular/core && ng update && npm update",
|
"start": "node dist/server/server.mjs",
|
||||||
"updateProjectVersion": "npm version patch --no-git-tag-version && echo SUCCEED UPDATE VERSION",
|
"------ CONFIGS": "",
|
||||||
"buildDevelopment": "npm run clean && ng build --configuration development && echo SUCCEED DEV BUILD && npm run updateProjectVersion",
|
"prepare": "husky"
|
||||||
"buildProduction": "npm run clean && ng build --configuration production && echo SUCCEED PROD BUILD && npm run updateProjectVersion",
|
},
|
||||||
"start": "node dist/server/server.mjs"
|
"lint-staged": {
|
||||||
|
"*.{ts,js}": [
|
||||||
|
"prettier --write",
|
||||||
|
"eslint --fix"
|
||||||
|
],
|
||||||
|
"*.{html,css,scss,json,md}": [
|
||||||
|
"prettier --write"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"repository": "https://github.com/FrauJulian/Personal-Portfolio-Website",
|
"repository": "https://github.com/FrauJulian/Personal-Portfolio-Website",
|
||||||
"bugs": "https://github.com/FrauJulian/Personal-Portfolio-Website/issues",
|
"bugs": "https://github.com/FrauJulian/Personal-Portfolio-Website/issues",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Lechner Julian",
|
"name": "Lechner Julian",
|
||||||
"nickname": "FrauJulian",
|
"nickname": "FrauJulian",
|
||||||
"email": "contact@fraujulian.xyz",
|
"email": "fraujulian@lechner.top",
|
||||||
"website": "https://fraujulian.xyz/"
|
"website": "https://fraujulian.xyz/"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^19.2.14",
|
"@angular/animations": "^21.2.9",
|
||||||
"@angular/cdk": "^19.2.19",
|
"@angular/cdk": "^21.2.7",
|
||||||
"@angular/common": "^19.2.0",
|
"@angular/common": "^21.2.9",
|
||||||
"@angular/compiler": "^19.2.0",
|
"@angular/compiler": "^21.2.9",
|
||||||
"@angular/core": "^19.2.0",
|
"@angular/core": "^21.2.9",
|
||||||
"@angular/forms": "^19.2.0",
|
"@angular/forms": "^21.2.9",
|
||||||
"@angular/material": "^19.2.19",
|
"@angular/material": "^21.2.7",
|
||||||
"@angular/platform-browser": "^19.2.0",
|
"@angular/platform-browser": "^21.2.9",
|
||||||
"@angular/platform-browser-dynamic": "^19.2.0",
|
"@angular/platform-browser-dynamic": "^21.2.9",
|
||||||
"@angular/platform-server": "^19.2.0",
|
"@angular/platform-server": "^21.2.9",
|
||||||
"@angular/router": "^19.2.0",
|
"@angular/router": "^21.2.9",
|
||||||
"@angular/ssr": "^19.2.9",
|
"@angular/ssr": "^21.2.7",
|
||||||
"@fortawesome/angular-fontawesome": "^1.0.0",
|
"@fortawesome/angular-fontawesome": "^4.0.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||||
|
"@types/markdown-it": "^14.1.2",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.1",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^19.2.9",
|
"@angular-devkit/build-angular": "^21.2.7",
|
||||||
"@angular/cli": "^19.2.9",
|
"@angular/cli": "^21.2.7",
|
||||||
"@angular/compiler-cli": "^19.2.0",
|
"@angular/compiler-cli": "^21.2.9",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"@types/markdown-it": "^14.1.2",
|
|
||||||
"@types/node": "^18.18.0",
|
"@types/node": "^18.18.0",
|
||||||
"@types/showdown": "^2.0.6",
|
"@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",
|
"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",
|
"jasmine-core": "~5.6.0",
|
||||||
"karma": "~6.4.0",
|
"karma": "~6.4.0",
|
||||||
"karma-chrome-launcher": "~3.2.0",
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
"karma-coverage": "~2.2.0",
|
"karma-coverage": "~2.2.0",
|
||||||
"karma-jasmine": "~5.1.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "~2.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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 873 KiB |
|
Before Width: | Height: | Size: 19 KiB |
@@ -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);
|
||||||
@@ -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)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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 = [
|
||||||
|
['Ä', 'Ä'],
|
||||||
|
['Ö', 'Ö'],
|
||||||
|
['Ü', 'Ü'],
|
||||||
|
['ä', 'ä'],
|
||||||
|
['ö', 'ö'],
|
||||||
|
['ü', 'ü'],
|
||||||
|
['ß', 'ß'],
|
||||||
|
['’', '’'],
|
||||||
|
['“', '“'],
|
||||||
|
['â€', '”'],
|
||||||
|
['–', '–'],
|
||||||
|
['—', '—'],
|
||||||
|
['…', '…'],
|
||||||
|
['§', '§'],
|
||||||
|
['Ä', 'Ä'],
|
||||||
|
['Ö', 'Ö'],
|
||||||
|
['Ü', 'Ü'],
|
||||||
|
['ä', 'ä'],
|
||||||
|
['ö', 'ö'],
|
||||||
|
['ü', 'ü'],
|
||||||
|
['ß', 'ß'],
|
||||||
|
['§', '§'],
|
||||||
|
];
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -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, '&')
|
||||||
|
.replace(/"/gu, '"')
|
||||||
|
.replace(/'/gu, ''')
|
||||||
|
.replace(/</gu, '<')
|
||||||
|
.replace(/>/gu, '>');
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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 => {
|
describe('AppComponent', (): void => {
|
||||||
beforeEach(async (): Promise<void> => {
|
beforeEach(async (): Promise<void> => {
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import {Component} from '@angular/core'
|
import { Component } from '@angular/core';
|
||||||
import {RouterOutlet} from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
imports: [
|
imports: [RouterOutlet],
|
||||||
RouterOutlet
|
templateUrl: './app.component.html',
|
||||||
],
|
|
||||||
templateUrl: './app.component.html'
|
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
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/ssr';
|
||||||
import {provideServerRendering} from '@angular/platform-server';
|
import type { ApplicationConfig } from '@angular/core';
|
||||||
|
import { mergeApplicationConfig } from '@angular/core';
|
||||||
|
|
||||||
import {appConfig} from './app.config';
|
import { appConfig } from './app.config';
|
||||||
|
|
||||||
const serverConfig: ApplicationConfig = {
|
const serverConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [provideServerRendering()],
|
||||||
provideServerRendering(),
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const config: ApplicationConfig = mergeApplicationConfig(appConfig, serverConfig);
|
export const config: ApplicationConfig = mergeApplicationConfig(appConfig, serverConfig);
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import {ApplicationConfig, importProvidersFrom, provideZoneChangeDetection} from '@angular/core';
|
import type { ApplicationConfig } from '@angular/core';
|
||||||
import {provideRouter, withInMemoryScrolling} from '@angular/router';
|
import { importProvidersFrom, provideZoneChangeDetection } from '@angular/core';
|
||||||
import {BrowserModule, provideClientHydration, withEventReplay} from '@angular/platform-browser';
|
import { provideRouter, withInMemoryScrolling } from '@angular/router';
|
||||||
|
import { BrowserModule, provideClientHydration, withEventReplay } from '@angular/platform-browser';
|
||||||
|
|
||||||
import {routes} from './app.routes';
|
import { routes } from './app.routes';
|
||||||
import {provideAnimations} from '@angular/platform-browser/animations';
|
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideZoneChangeDetection({eventCoalescing: true}),
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
provideRouter(routes, withInMemoryScrolling({
|
provideRouter(
|
||||||
anchorScrolling: 'enabled',
|
routes,
|
||||||
scrollPositionRestoration: 'top'
|
withInMemoryScrolling({
|
||||||
})),
|
anchorScrolling: 'enabled',
|
||||||
|
scrollPositionRestoration: 'top',
|
||||||
|
}),
|
||||||
|
),
|
||||||
provideClientHydration(withEventReplay()),
|
provideClientHydration(withEventReplay()),
|
||||||
importProvidersFrom(BrowserModule),
|
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 { HomeComponent } from './home/home.component';
|
||||||
import {ImprintComponent} from './imprint/imprint.component';
|
import { ImprintComponent } from './imprint/imprint.component';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{path: '', component: HomeComponent, pathMatch: 'full'},
|
{ path: '', component: HomeComponent, pathMatch: 'full' },
|
||||||
{path: 'imprint', component: ImprintComponent, pathMatch: 'full'},
|
{ path: 'imprint', component: ImprintComponent, pathMatch: 'full' },
|
||||||
{path: '**', redirectTo: ''}
|
{ path: '**', redirectTo: '' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
<noscript>
|
<noscript>
|
||||||
<div class="container">
|
<div class="page-shell footer-shell">
|
||||||
<div class="card">
|
<div class="panel noscript-panel">
|
||||||
<h2 id="loading-text" class="warning-text">
|
<h2 id="loading-text" class="warning-text">Enable JavaScript for the full experience.</h2>
|
||||||
Enable javascript for the full experience!
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|
||||||
<footer>
|
<footer class="site-footer">
|
||||||
<p>
|
<div class="footer-inner">
|
||||||
<a routerLink="/imprint">IMPRINT</a> | © 2023
|
<p>© 2023 - {{ currentYear }} {{ global.firstname }} {{ global.lastname }}</p>
|
||||||
- {{ currentYear }} {{ globalFields.firstname }} {{ globalFields.lastname }} - All rights reserved.
|
<a routerLink="/imprint">Imprint</a>
|
||||||
</p>
|
</div>
|
||||||
</footer>
|
</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 => {
|
describe('FooterComponent', (): void => {
|
||||||
let component: FooterComponent;
|
let component: FooterComponent;
|
||||||
@@ -8,9 +9,8 @@ describe('FooterComponent', (): void => {
|
|||||||
|
|
||||||
beforeEach(async (): Promise<void> => {
|
beforeEach(async (): Promise<void> => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [FooterComponent]
|
imports: [FooterComponent],
|
||||||
})
|
}).compileComponents();
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(FooterComponent);
|
fixture = TestBed.createComponent(FooterComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import {Component} from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import {RouterLink} from "@angular/router";
|
import { RouterLink } from '@angular/router';
|
||||||
import {globalFields} from '../../global.fields';
|
import type { Global } from '../../global.fields';
|
||||||
|
import { global } from '../../global.fields';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-footer',
|
selector: 'app-footer',
|
||||||
imports: [
|
imports: [RouterLink],
|
||||||
RouterLink
|
templateUrl: './footer.component.html',
|
||||||
],
|
|
||||||
templateUrl: './footer.component.html'
|
|
||||||
})
|
})
|
||||||
export class FooterComponent {
|
export class FooterComponent {
|
||||||
protected readonly globalFields: typeof globalFields = globalFields;
|
protected readonly global: Global = global;
|
||||||
|
|
||||||
protected readonly currentYear: number = new Date().getFullYear();
|
protected readonly currentYear: number = new Date().getFullYear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,146 +1,156 @@
|
|||||||
<section class="container">
|
<main class="page-shell home-page">
|
||||||
<div class="card profile">
|
<section class="hero-section panel">
|
||||||
|
<div class="hero-copy">
|
||||||
<img
|
<div class="hero-copy-main">
|
||||||
ngSrc="Logo.webp"
|
<p class="eyebrow">Portfolio</p>
|
||||||
width="200"
|
<h1>
|
||||||
height="200"
|
<span class="hero-name-part">{{ global.firstname }}</span>
|
||||||
priority
|
<span class="hero-name-part">{{ global.lastname }}</span>
|
||||||
fetchpriority="high"
|
</h1>
|
||||||
alt="profile picture"
|
<p class="hero-intro">
|
||||||
srcset="Logo.webp 1x, Logo.webp 2x"
|
Full-stack development, thoughtful interfaces, and tailored digital products for companies, associations, and private clients.
|
||||||
(click)="openStalkerInfo()"/>
|
</p>
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="long-bio-text" *ngIf="isLongBioShown" @fadeInOut (click)="closeOnOverlayClick($event)">
|
<div class="hero-actions">
|
||||||
<p>
|
<div class="contact-area hero-contact-area">
|
||||||
My name is {{ globalFields.firstname }}, many people also call me Julie. I'm {{ age }} years old and come from
|
<a [href]="contactSafeMail" aria-label="Send mail">
|
||||||
beautiful Austria.
|
<fa-icon class="fab" [icon]="faEnvelope" size="lg"></fa-icon>
|
||||||
I travel extensively within Austria, so I spend a great deal of my time on trains.
|
</a>
|
||||||
<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>
|
|
||||||
|
|
||||||
<hr>
|
<a href="tel:{{ global.hrefContactPhone }}" aria-label="Call phone number">
|
||||||
<br>
|
<fa-icon class="fab" [icon]="faPhone" size="lg"></fa-icon>
|
||||||
|
</a>
|
||||||
|
|
||||||
<div class="bio grid-container">
|
<a href="https://discord.com/users/860206216893693973" aria-label="Discord profile">
|
||||||
<div class="grid-item-start">
|
<fa-icon class="fab" [icon]="faDiscord" size="lg"></fa-icon>
|
||||||
<h1>I work with</h1>
|
</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>
|
||||||
<div class="grid-item" *ngFor="let bioText of bioTextsList; let i = index" [hidden]="i !== currentIndex">
|
|
||||||
<h1>{{ bioText }}</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="contact-area">
|
<div class="hero-meta">
|
||||||
<a [href]="contactSafeMail">
|
<div>
|
||||||
<fa-icon class="fab" [icon]="faEnvelope" size="3x"></fa-icon>
|
<span class="meta-label">my stack</span>
|
||||||
</a>
|
<strong>{{ global.bioTextsList[currentIndex] }}</strong>
|
||||||
|
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</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>
|
<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 => {
|
describe('HomeComponent', (): void => {
|
||||||
let component: HomeComponent;
|
let component: HomeComponent;
|
||||||
@@ -8,9 +9,8 @@ describe('HomeComponent', (): void => {
|
|||||||
|
|
||||||
beforeEach(async (): Promise<void> => {
|
beforeEach(async (): Promise<void> => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [HomeComponent]
|
imports: [HomeComponent],
|
||||||
})
|
}).compileComponents();
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(HomeComponent);
|
fixture = TestBed.createComponent(HomeComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
|||||||
@@ -1,148 +1,63 @@
|
|||||||
import {
|
import type { OnDestroy, OnInit } from '@angular/core';
|
||||||
Component,
|
import { Component, inject, NgZone } from '@angular/core';
|
||||||
NgZone,
|
import { faArrowRight, faArrowDown, faEnvelope, faPhone } from '@fortawesome/free-solid-svg-icons';
|
||||||
OnDestroy,
|
import { faDiscord, faGithub, faLinkedin } from '@fortawesome/free-brands-svg-icons';
|
||||||
OnInit,
|
import type { IconDefinition } from '@fortawesome/angular-fontawesome';
|
||||||
} from '@angular/core';
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||||
import {trigger, transition, style, animate} from '@angular/animations';
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
import {faArrowRight, faArrowLeft, faArrowDown, faStar, faEnvelope, faPhone} from '@fortawesome/free-solid-svg-icons';
|
import type { SafeUrl } from '@angular/platform-browser';
|
||||||
import {
|
import type { Subscription } from 'rxjs';
|
||||||
faDiscord,
|
import { interval } from 'rxjs';
|
||||||
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 {globalFields} from '../../global.fields';
|
import type { Global, PortraitHighlight, PortfolioProject } from '../../global.fields';
|
||||||
import {FooterComponent} from '../footer/footer.component';
|
import { global } from '../../global.fields';
|
||||||
import {MatDialog} from '@angular/material/dialog';
|
import { FooterComponent } from '../footer/footer.component';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-home',
|
selector: 'app-home',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [FooterComponent, FaIconComponent],
|
||||||
NgOptimizedImage,
|
|
||||||
FooterComponent,
|
|
||||||
NgIf,
|
|
||||||
FaIconComponent,
|
|
||||||
NgForOf
|
|
||||||
],
|
|
||||||
templateUrl: './home.component.html',
|
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 {
|
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 isLongBioShown: boolean = false;
|
||||||
protected isApiRateLimitExceeded: boolean = false;
|
|
||||||
protected readonly faArrowRight: IconDefinition = faArrowRight;
|
protected readonly faArrowRight: IconDefinition = faArrowRight;
|
||||||
protected readonly faArrowLeft: IconDefinition = faArrowLeft;
|
|
||||||
protected readonly faArrowDown: IconDefinition = faArrowDown;
|
protected readonly faArrowDown: IconDefinition = faArrowDown;
|
||||||
protected readonly faStar: IconDefinition = faStar;
|
|
||||||
protected readonly faEnvelope: IconDefinition = faEnvelope;
|
protected readonly faEnvelope: IconDefinition = faEnvelope;
|
||||||
protected readonly faPhone: IconDefinition = faPhone;
|
protected readonly faPhone: IconDefinition = faPhone;
|
||||||
readonly faDiscord: IconDefinition = faDiscord;
|
readonly faDiscord: IconDefinition = faDiscord;
|
||||||
readonly faGithub: IconDefinition = faGithub;
|
readonly faGithub: IconDefinition = faGithub;
|
||||||
readonly faXing: IconDefinition = faXing;
|
|
||||||
readonly faLinkedin: IconDefinition = faLinkedin;
|
readonly faLinkedin: IconDefinition = faLinkedin;
|
||||||
|
|
||||||
protected readonly contactSafeMail: SafeUrl;
|
protected readonly contactSafeMail: SafeUrl;
|
||||||
|
protected readonly portraitHighlights: PortraitHighlight[] = global.portraitHighlights;
|
||||||
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!)';
|
protected currentPortraitHighlightIndex: number = 0;
|
||||||
readonly bioTextsList: string[] = [
|
protected isPortraitSwitching: boolean = false;
|
||||||
'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 currentIndex = 0;
|
protected currentIndex = 0;
|
||||||
|
protected readonly projects: PortfolioProject[] = global.projects;
|
||||||
private sub!: Subscription;
|
private sub!: Subscription;
|
||||||
|
|
||||||
protected projects: Project[] = [
|
constructor() {
|
||||||
{
|
this.contactSafeMail = this.sanitizer.bypassSecurityTrustUrl(`mailto:${global.contactMail}`);
|
||||||
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}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.preloadImageAssets();
|
||||||
|
|
||||||
this.zone.runOutsideAngular((): void => {
|
this.zone.runOutsideAngular((): void => {
|
||||||
this.sub = interval(1000).subscribe((): void => {
|
this.sub = interval(1000).subscribe((): void => {
|
||||||
this.zone.run((): 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();
|
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 {
|
protected toggleBio(): void {
|
||||||
this.isLongBioShown = !this.isLongBioShown;
|
this.isLongBioShown = !this.isLongBioShown;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected closeOnOverlayClick(evt: MouseEvent): void {
|
protected get currentPortraitHighlight(): PortraitHighlight | null {
|
||||||
if ((evt.target as HTMLElement).classList.contains('long-bio-overlay')) {
|
return this.portraitHighlights[this.currentPortraitHighlightIndex] ?? null;
|
||||||
this.isLongBioShown = false;
|
}
|
||||||
|
|
||||||
|
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 {
|
private calculateAge(birthDateString: string): number | string {
|
||||||
|
|||||||
@@ -1,76 +1,85 @@
|
|||||||
<div class="container">
|
<main class="page-shell imprint-page">
|
||||||
<div class="card profile">
|
<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>
|
<article class="panel legal-card">
|
||||||
{{ street }} {{ houseNumber }} <br>
|
<p class="eyebrow">Contact</p>
|
||||||
{{ zip }} {{ city }} <br>
|
<p class="legal-row">
|
||||||
{{ country }} <br>
|
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>
|
<section class="panel legal-copy">
|
||||||
|
<div class="legal-block">
|
||||||
<h4>Contact:</h4>
|
<h2>Scope of Website</h2>
|
||||||
Phone: <b><u><a href="tel:{{globalFields.hrefContactTel}}">{{ globalFields.contactPhone }}</a></u></b> <br>
|
<p>This website is a personal portfolio and a contact point for software engineering and related digital services.</p>
|
||||||
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>
|
|
||||||
</div>
|
</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>
|
<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';
|
import { ImprintComponent } from './imprint.component';
|
||||||
|
|
||||||
@@ -8,9 +9,8 @@ describe('ImprintComponent', (): void => {
|
|||||||
|
|
||||||
beforeEach(async (): Promise<void> => {
|
beforeEach(async (): Promise<void> => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [ImprintComponent]
|
imports: [ImprintComponent],
|
||||||
})
|
}).compileComponents();
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(ImprintComponent);
|
fixture = TestBed.createComponent(ImprintComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
|||||||
@@ -1,41 +1,33 @@
|
|||||||
import {Component, OnInit} from '@angular/core';
|
import type { OnInit } from '@angular/core';
|
||||||
import {FooterComponent} from '../footer/footer.component';
|
import { Component, inject } from '@angular/core';
|
||||||
import {RouterLink} from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import {globalFields} from '../../global.fields';
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
import {DomSanitizer, SafeUrl} from '@angular/platform-browser';
|
import type { SafeUrl } from '@angular/platform-browser';
|
||||||
import {FaIconComponent, IconDefinition} from '@fortawesome/angular-fontawesome';
|
import type { IconDefinition } from '@fortawesome/angular-fontawesome';
|
||||||
import {faArrowDown, faArrowLeft, faArrowRight} from '@fortawesome/free-solid-svg-icons';
|
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({
|
@Component({
|
||||||
selector: 'app-imprint',
|
selector: 'app-imprint',
|
||||||
imports: [
|
imports: [FooterComponent, FaIconComponent, RouterLink],
|
||||||
FooterComponent,
|
templateUrl: './imprint.component.html',
|
||||||
FaIconComponent,
|
|
||||||
RouterLink
|
|
||||||
],
|
|
||||||
templateUrl: './imprint.component.html'
|
|
||||||
})
|
})
|
||||||
export class ImprintComponent implements OnInit {
|
export class ImprintComponent implements OnInit {
|
||||||
protected readonly globalFields: typeof globalFields = globalFields;
|
private readonly sanitizer = inject(DomSanitizer);
|
||||||
|
|
||||||
protected readonly street: string = 'Ulmenstraße';
|
protected readonly global: Global = global;
|
||||||
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 contactSafeMail!: SafeUrl;
|
protected contactSafeMail!: SafeUrl;
|
||||||
protected abuseSafeMail!: SafeUrl;
|
protected abuseSafeMail!: SafeUrl;
|
||||||
|
|
||||||
protected readonly faArrowRight: IconDefinition = faArrowRight;
|
|
||||||
protected readonly faArrowLeft: IconDefinition = faArrowLeft;
|
protected readonly faArrowLeft: IconDefinition = faArrowLeft;
|
||||||
|
|
||||||
constructor(private _sanitizer: DomSanitizer) {
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.contactSafeMail = this._sanitizer.bypassSecurityTrustUrl(`mailto:${globalFields.contactMail}`);
|
this.contactSafeMail = this.sanitizer.bypassSecurityTrustUrl(`mailto:${global.contactMail}`);
|
||||||
this.abuseSafeMail = this._sanitizer.bypassSecurityTrustUrl(`mailto:${this.abuseMail}`);
|
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 5.0 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 809 KiB |
|
After Width: | Height: | Size: 9.2 MiB |
|
After Width: | Height: | Size: 180 KiB |
@@ -1,9 +1,138 @@
|
|||||||
export class globalFields {
|
export interface PortfolioProject {
|
||||||
public static readonly firstname: string = 'Julian';
|
title: string;
|
||||||
public static readonly lastname: string = 'Lechner';
|
link: string;
|
||||||
public static readonly contactMail: string = 'office@lechner-systems.at';
|
description: string;
|
||||||
public static readonly contactPhone: string = '+43 (0) 660 9254001';
|
languages: string[];
|
||||||
public static readonly hrefContactTel: string = '+436609254001';
|
icon?: string;
|
||||||
public static readonly githubUsername: string = 'FrauJulian';
|
CircleIcon?: boolean;
|
||||||
public static readonly numberOfLoadedRepositories: number = 999;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,24 +1,14 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<base href="/">
|
<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>
|
<title>Lechner Julian</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
<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>
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import {bootstrapApplication} from '@angular/platform-browser';
|
import type { BootstrapContext } from '@angular/platform-browser';
|
||||||
import {ApplicationRef} from '@angular/core';
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
|
import type { ApplicationRef } from '@angular/core';
|
||||||
|
import { provideZoneChangeDetection } from '@angular/core';
|
||||||
|
|
||||||
import {AppComponent} from './app/app.component';
|
import { AppComponent } from './app/app.component';
|
||||||
import {config} from './app/app.config.server';
|
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;
|
export default bootstrap;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {bootstrapApplication} from '@angular/platform-browser';
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
|
|
||||||
import {appConfig} from './app/app.config';
|
import { appConfig } from './app/app.config';
|
||||||
import {AppComponent} from './app/app.component';
|
import { AppComponent } from './app/app.component';
|
||||||
|
|
||||||
bootstrapApplication(AppComponent, appConfig)
|
bootstrapApplication(AppComponent, appConfig).catch((err: Error): void => console.error(err.stack));
|
||||||
.catch((err: Error): void => console.error(err.stack));
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
User-agent: *
|
||||||
|
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://fraujulian.xyz/sitemap.xml
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import {APP_BASE_HREF} from '@angular/common';
|
import { APP_BASE_HREF } from '@angular/common';
|
||||||
import {CommonEngine, isMainModule} from '@angular/ssr/node';
|
import { CommonEngine, isMainModule } from '@angular/ssr/node';
|
||||||
import express, {Express, NextFunction, Request, Response} from 'express';
|
import type { Express, NextFunction, Request, Response } from 'express';
|
||||||
import {dirname, join, resolve} from 'node:path';
|
import express from 'express';
|
||||||
import {fileURLToPath} from 'node:url';
|
import { dirname, join, resolve } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
import bootstrap from './main.server';
|
import bootstrap from './main.server';
|
||||||
import ProductionConfig from '../configs/Production.json';
|
import ProductionConfig from '../configs/Production.json';
|
||||||
@@ -18,12 +19,12 @@ app.get(
|
|||||||
'**',
|
'**',
|
||||||
express.static(browserDistFolder, {
|
express.static(browserDistFolder, {
|
||||||
maxAge: '1y',
|
maxAge: '1y',
|
||||||
index: 'index.html'
|
index: 'index.html',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
app.get('**', (req: Request, res: Response, next: NextFunction): void => {
|
app.get('**', (req: Request, res: Response, next: NextFunction): void => {
|
||||||
const {protocol, originalUrl, baseUrl, headers} = req;
|
const { protocol, originalUrl, baseUrl, headers } = req;
|
||||||
|
|
||||||
commonEngine
|
commonEngine
|
||||||
.render({
|
.render({
|
||||||
@@ -31,7 +32,7 @@ app.get('**', (req: Request, res: Response, next: NextFunction): void => {
|
|||||||
documentFilePath: indexHtml,
|
documentFilePath: indexHtml,
|
||||||
url: `${protocol}://${headers.host}${originalUrl}`,
|
url: `${protocol}://${headers.host}${originalUrl}`,
|
||||||
publicPath: browserDistFolder,
|
publicPath: browserDistFolder,
|
||||||
providers: [{provide: APP_BASE_HREF, useValue: baseUrl}],
|
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
|
||||||
})
|
})
|
||||||
.then((html: string): Response => res.send(html))
|
.then((html: string): Response => res.send(html))
|
||||||
.catch((err: Error): void => next(err.stack));
|
.catch((err: Error): void => next(err.stack));
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -4,16 +4,8 @@
|
|||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./out-tsc/app",
|
"outDir": "./out-tsc/app",
|
||||||
"types": [
|
"types": ["node"]
|
||||||
"node"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"files": [
|
"files": ["src/main.ts", "src/main.server.ts", "src/server.ts"],
|
||||||
"src/main.ts",
|
"include": ["src/**/*.d.ts"]
|
||||||
"src/main.server.ts",
|
|
||||||
"src/server.ts"
|
|
||||||
],
|
|
||||||
"include": [
|
|
||||||
"src/**/*.d.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.d.ts"],
|
||||||
|
"exclude": ["src/**/*.spec.ts", "src/test.ts"]
|
||||||
|
}
|
||||||
@@ -4,12 +4,7 @@
|
|||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./out-tsc/spec",
|
"outDir": "./out-tsc/spec",
|
||||||
"types": [
|
"types": ["jasmine"]
|
||||||
"jasmine"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
|
||||||
"src/**/*.spec.ts",
|
|
||||||
"src/**/*.d.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||