Compare commits

...
Author SHA1 Message Date
fraujulian cad41e5dc0 chore: updated LICENSE 2026-05-15 15:42:50 +02:00
fraujulian c99d086745 chore: updated README.md 2026-05-15 15:36:49 +02:00
fraujulian f0463229a1 adding opgenpgp key 2026-05-15 15:35:31 +02:00
fraujulian 17cb23aa40 refactorings 2026-05-15 14:06:38 +02:00
29 changed files with 1350 additions and 732 deletions
+1
View File
@@ -12,6 +12,7 @@
.angular .angular
node_modules node_modules
dist dist
public
.npm-cache .npm-cache
coverage coverage
.eslintcache .eslintcache
-1
View File
@@ -1,3 +1,2 @@
IMAGE_TAG=latest IMAGE_TAG=latest
MAIN_PORT=3000 MAIN_PORT=3000
NG_ALLOWED_HOSTS=fraujulian.xyz,www.fraujulian.xyz,test.fraujulian.xyz
+3 -5
View File
@@ -33,8 +33,10 @@ jobs:
run: | run: |
version=$(node -p 'require("./package.json").version') version=$(node -p 'require("./package.json").version')
docker_tag=$version docker_tag=$version
revision=${GITHUB_RUN_ATTEMPT}
echo "value=$version" >> "$GITHUB_OUTPUT" echo "value=$version" >> "$GITHUB_OUTPUT"
echo "docker_tag=$docker_tag" >> "$GITHUB_OUTPUT" echo "docker_tag=$docker_tag" >> "$GITHUB_OUTPUT"
echo "revision=$revision" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@@ -54,10 +56,6 @@ jobs:
provenance: false provenance: false
build-args: | build-args: |
APP_VERSION=${{ steps.version.outputs.value }} APP_VERSION=${{ steps.version.outputs.value }}
VCS_REF=${{ steps.version.outputs.revision }}
tags: | tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev ${{ 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 }}
+3 -5
View File
@@ -32,8 +32,10 @@ jobs:
run: | run: |
version=$(node -p 'require("./package.json").version') version=$(node -p 'require("./package.json").version')
docker_tag=${version//+/-} docker_tag=${version//+/-}
revision=${GITHUB_RUN_ATTEMPT}
echo "value=$version" >> "$GITHUB_OUTPUT" echo "value=$version" >> "$GITHUB_OUTPUT"
echo "docker_tag=$docker_tag" >> "$GITHUB_OUTPUT" echo "docker_tag=$docker_tag" >> "$GITHUB_OUTPUT"
echo "revision=$revision" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@@ -53,13 +55,9 @@ jobs:
provenance: false provenance: false
build-args: | build-args: |
APP_VERSION=${{ steps.version.outputs.value }} APP_VERSION=${{ steps.version.outputs.value }}
VCS_REF=${{ steps.version.outputs.revision }}
tags: | tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest ${{ 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 - name: Create GitHub release
env: env:
+1
View File
@@ -9,6 +9,7 @@
# Generierte Build- und Test-Ausgaben # Generierte Build- und Test-Ausgaben
/dist /dist
/coverage /coverage
/public
/src/assets/optimized /src/assets/optimized
# Betriebssystem-Dateileichen # Betriebssystem-Dateileichen
+21 -21
View File
@@ -1,39 +1,39 @@
# Paketmanager-Abhaengigkeiten werden nicht formatiert # Dependencies
/node_modules /node_modules
# Lokale Tool-Caches # Build, coverage, and generated output
.npm-cache
.eslintcache
# Generierte Build- und Test-Ausgaben
/dist /dist
/coverage /coverage
/.angular
/public
src/assets/Resources/CSS/Tailwind.css
src/assets/optimized
# Betriebssystem-Dateileichen # Package manager and tool caches
.DS_Store .npm-cache
Thumbs.db .eslintcache
.prettier-cache
*.tsbuildinfo
# Editor- und IDE-Konfigurationen # Logs and debug output
/.idea *.log
/.vscode
# Lokale Log- und Debug-Dateien
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
*.log
*.tsbuildinfo
# Lokale Secrets und Umgebungsdateien # Local environment and secrets
.env .env
.env.* .env.*
backend/.env backend/.env
backend/.env.* backend/.env.*
# Angular-CLI-Cache # Editor, IDE, and OS files
/.angular /.idea
/.vscode
.DS_Store
Thumbs.db
# Generierte Tailwind-Ausgabe wird aus dem Quell-CSS erzeugt # Markdown is maintained manually and excluded from Prettier formatting
src/assets/Resources/CSS/Tailwind.css *.md
+155 -140
View File
@@ -1,208 +1,223 @@
# Repository Instructions # Repository Guidelines
These instructions apply to this repository unless a more specific `AGENTS.md` ## Project Context
exists in a subdirectory.
## Required Verification This is an Angular portfolio application using Angular 21, TypeScript, SCSS,
Karma/Jasmine tests, and static hosting for production output. Production
application code lives in `src/app`; the client entry point lives in
`src/main.ts`.
- Always verify changes with: Feature views live in folders such as `src/app/home`, `src/app/footer`, and
- `npm run check` `src/app/imprint`. Reusable generic UI building blocks belong in
- `npm run test` `src/app/shared`; reusable services belong in `src/app/services`. Assets and
- If verification fails, `npm run fix` may help, but do not assume it resolves language content live in `src/assets` and `src/languages`. Do not manually
every issue. maintain generated or copied output in `public/assets/optimized` or `dist`.
- Respect the existing `tsconfig`, ESLint, and Prettier configurations.
## Assets ## Agent Workflow
- Do not edit generated optimized assets by hand. - Read relevant project files before making changes and follow existing
- Update source assets in `src/assets/unoptimized/`. patterns.
- Regenerate optimized assets through the provided scripts, usually via - Always validate every change with `npm run check`.
`npm run generate:assets` or commands that already include asset generation. - Do not run a build unless the user explicitly asks for it.
- If `npm run check` fails, read the cause first. `npm run fix` may be used, but
it cannot fix every issue.
- Always follow the active ESLint, Prettier, and `tsconfig` rules.
- Change only files that are relevant to the task.
- Do not revert unrelated or unclear local changes.
- Commit, push, or open PRs only when explicitly asked.
## Localization ## Important Commands
- Keep localized text synchronized between: - `npm run dev`: generates assets and starts the Angular development server.
- `src/languages/de.ts` - `npm run build`: generates assets, builds the production app, and copies
- `src/languages/en.ts` static root files. Run this only when explicitly requested.
- When changing user-facing text, update both language files unless the change - `npm start`: serves the built static app through `scripts/static-server.mjs`.
is intentionally language-specific. - `npm test`: generates assets and starts Angular tests with Karma/Jasmine.
- `npm run check`: runs linting, format checks, version checks, and source text
checks. Always use this as the final validation.
- `npm run fix`: places version data, runs ESLint fixes, fixes source text, and
formats the project.
## Architecture Use Node `>=24.14.0` and npm `>=11.12.0`.
- Create components with exactly one clear responsibility. ## Code Style
- Keep components focused on UI, user interaction, and view state.
- Move business logic into focused services.
- Structure code by feature or domain where appropriate.
- Put reusable generic elements in `shared/`.
- Put domain-specific logic and components in the relevant feature folder.
- Separate container and presentational components when a view becomes complex.
- Avoid cyclic dependencies between modules, services, and components.
- Add abstractions only when at least two real use cases exist.
- Keep services focused on one business or technical responsibility.
## TypeScript - TypeScript strict mode is enabled. `strictTemplates` is enabled.
- Prettier is authoritative: 2 spaces, single quotes, semicolons, trailing
- Keep strict TypeScript enabled. commas, and default print width 100.
- Use explicit types for public methods, service APIs, and complex return - Markdown wraps at 80 characters.
values. - Angular selectors use the `app` prefix.
- Prefer `unknown` over `any` when a value still needs validation. - Components follow the pattern `feature.component.ts`,
- Use `any` only when technically necessary and document the reason. `feature.component.html`, and `feature.component.spec.ts`.
- Use union types for fixed value ranges. - Services follow the pattern `name.service.ts`.
- Use `readonly` for data that should not be mutated. - Place shared types in `*.types.ts` files when that matches the local pattern.
- Prefer `const` over `let` when reassignment is not needed. - Explicitly type public methods, service APIs, and complex return values.
- Check `null` and `undefined` explicitly before accessing nested values. - Use `unknown` instead of `any` when a value must be checked first.
- Separate API DTOs from internal UI or domain models. - Use `any` only when technically necessary, and document the reason.
- Map external API data deliberately into internal models. - Use `const` instead of `let` when reassignment is not needed.
- Use clear names for interfaces, types, classes, variables, inputs, and - Use `readonly` for data that should not be changed.
outputs. - Do not use non-null assertions to hide real nullability issues.
- Avoid type casts when clean type narrowing is possible.
- Do not suppress type errors with `as unknown as`. - Do not suppress type errors with `as unknown as`.
- Do not use non-null assertions to hide real nullability problems. - Use union types for fixed value sets instead of magic strings.
- Keep API DTOs separate from internal UI or domain models.
## Angular Architecture
- Components must have exactly one clear responsibility.
- Keep components primarily responsible for UI, user interaction, and view state.
- Put business logic, HTTP access, mapping, and shared state into focused
services.
- Structure code by features or domains, not only by file types.
- Put domain-specific logic and domain-specific components in the relevant
feature folder.
- Put generic reusable elements in `shared/`.
- Split container components from presentational components when a view becomes
complex.
- Create abstractions only when at least two real use cases exist.
- Avoid cyclic dependencies between components, services, and modules.
- Do not use services as unordered global storage.
## Angular Components ## Angular Components
- Use `OnPush` change detection unless there is a clear reason not to. - Use `ChangeDetectionStrategy.OnPush` unless there is a concrete reason not to.
- Use inputs for externally provided data. - Use required inputs when a component cannot work meaningfully without the
- Use outputs for events emitted to parent components. data.
- Use required inputs when a component cannot work without the data. - Name inputs and outputs clearly after their purpose.
- Do not mutate input data directly.
- Keep component classes small and readable. - Keep component classes small and readable.
- Extract repeated UI blocks into dedicated components. - Extract repeated UI blocks into their own components.
- Use lifecycle hooks only when they are necessary. - Use lifecycle hooks only when they are truly necessary.
- Keep constructors limited to dependency injection. - Keep constructors free of logic; prefer dependency injection with `inject()`
- Initialize data clearly and predictably. when it fits the existing code.
- Do not make god components. - Do not manipulate the DOM directly when Angular APIs are sufficient.
- Do not mix API access, mapping, business logic, and UI logic in one component. - Do not use component inheritance when composition is simpler.
- Prefer composition over component inheritance.
## Angular State ## State, RxJS, and Data Flows
- Use Signals for local synchronous component state. - Use Signals for local synchronous component state.
- Use computed Signals for derived synchronous values.
- Use RxJS for asynchronous data streams and complex event chains. - Use RxJS for asynchronous data streams and complex event chains.
- Use services for shared state across multiple components. - Encapsulate shared state in services.
- Keep state as close as possible to where it is needed. - Keep state as close as possible to where it is needed.
- Avoid global state when local state is enough. - Make state changes explicit and easy to follow.
- Make state changes explicit and understandable.
- Avoid hidden state mutation across multiple services.
- Do not store derived values redundantly when they can be computed.
## RxJS
- Name Observables with a `$` suffix. - Name Observables with a `$` suffix.
- Use the `async` pipe when a stream is displayed directly in a template. - Use the `async` pipe when a stream is displayed directly in the template.
- Use `switchMap` for requests where older requests should be canceled. - Choose `switchMap`, `concatMap`, `mergeMap`, and `exhaustMap` deliberately
- Use `concatMap` when ordering must be guaranteed. based on their semantics.
- Use `mergeMap` when parallel processing is intended. - Use `combineLatest` for dependent live values and `forkJoin` for multiple
- Use `exhaustMap` when new events should be ignored while work is running. one-time requests.
- Use `combineLatest` for dependent live values. - End manual subscriptions with `takeUntilDestroyed` or an equivalent mechanism.
- Use `forkJoin` for multiple one-time requests that complete together. - Do not write `subscribe()` inside `subscribe()`.
- Clean up manual subscriptions with `takeUntilDestroyed` or an equivalent. - Handle errors with `catchError` in the right place and do not blindly swallow
- Handle errors with `catchError` at the appropriate level. them with `EMPTY`.
- Return controlled fallback state when useful.
- Do not hide errors that matter for debugging or UX.
- Do not use `subscribe()` inside `subscribe()`.
- Do not use RxJS for trivial synchronous state. - Do not use RxJS for trivial synchronous state.
- Avoid duplicate identical requests from independent subscriptions.
## HTTP and API ## HTTP and API
- Encapsulate all HTTP calls in services. - Encapsulate all HTTP calls in services.
- Define response types for API responses. - Define response types for API responses.
- Use central API configuration through environments or injection tokens. - Keep API configuration centralized through environments or injection tokens.
- Use interceptors for repeated technical concerns such as auth, headers, and - Use interceptors for recurring technical tasks such as auth, headers, or error
error handling. handling.
- Implement loading, error, and empty states where requests affect UI. - Include loading, error, and empty states for slow or failed requests.
- Treat API data defensively when quality is uncertain. - Treat external data defensively, and validate or normalize it before broad UI
- Validate or normalize external data before broad UI usage. use.
- Avoid duplicate API calls through shared streams or caching where appropriate. - Map external API data deliberately into internal models.
- Avoid duplicate API calls through shared stream or cache handling.
- Do not hardcode API URLs, tokens, roles, or secrets in components. - Do not hardcode API URLs, tokens, roles, or secrets in components.
## Templates ## Templates
- Keep templates declarative and easy to read. - Keep templates declarative and easy to read.
- Use templates for presentation and simple bindings only. - Use templates only for presentation and simple bindings.
- Move complex conditions into component code or computed signals. - Move complex conditions into component code or computed Signals.
- Use `@if` for conditional rendering. - Use `@if` for conditional rendering.
- Use `@for` with `track` for lists. - Always use `@for` with `track`.
- Track with a stable unique ID when available. - Use stable unique IDs for `track`; use the index only when no stable ID
exists.
- Do not put business rules, complex ternaries, or expensive method calls in
templates.
- Use pipes only for pure formatting.
- Break large templates into smaller components.
- Keep binding expressions short. - Keep binding expressions short.
- Do not put business rules or complex ternaries into templates.
- Do not call expensive methods directly from templates.
- Split large templates into smaller components.
## Forms ## Forms
- Use Reactive Forms for complex forms. - Use Reactive Forms for complex forms.
- Define validators explicitly. - Define validators explicitly.
- Show validation errors clearly in the UI. - Show validation errors clearly in the UI.
- Validate critical rules again on the backend.
- Keep form models and API DTOs separate when they differ. - Keep form models and API DTOs separate when they differ.
- Map form data deliberately before submitting. - Map form data deliberately before sending it.
- Disable submit while requests are running when duplicate submits are a risk. - Disable submit during active requests when repeated submission is problematic.
- Surface server-side validation errors in the form. - Show server errors visibly in the form.
## Styling ## Styling
- Prefer component-scoped styling. - Prefer component-scoped styling.
- Use global styles only for truly global rules. - Use global styles in `src/styles.scss` only for truly global rules.
- Use a consistent design system or consistent utility classes. - Use consistent design tokens, utility classes, or reusable components for
- Reuse classes or components for repeated UI patterns. recurring UI patterns.
- Account for responsive design in every new view. - Consider responsive design for every new view.
- Prefer semantic HTML before complex CSS. - Use semantic HTML before adding complex CSS.
- Keep spacing, colors, and font sizes consistent. - Keep spacing, colors, and font sizes consistent.
- Use inline styles only for dynamic exceptions. - Use inline styles only for dynamic edge cases.
- Do not use `::ng-deep` as a default solution. - Do not use `::ng-deep` as a default solution.
- Do not create global CSS rules that unintentionally affect other components. - Do not write global CSS rules that unintentionally affect unrelated
- Avoid fixed pixel layouts without responsive behavior. components.
- Do not build fixed-pixel layouts without responsive behavior.
## Security ## Security
- Treat all external data as potentially unsafe. - Treat all external data as potentially unsafe.
- Never trust user input without validation. - Never trust user input without validation.
- Do not store secrets in frontend code. - Do not store secrets in the frontend or repository.
- Implement roles and permissions server-side. - Use `.env.example` as the documented template; keep real environment-specific
- Use frontend role checks only for UX, not as a security boundary. values out of version control.
- Render API-provided HTML only after controlled sanitization. - Enforce roles and permissions server-side; use frontend checks only for UX.
- Use `bypassSecurityTrustHtml` only after explicit security review. - Render HTML from APIs only after controlled sanitization.
- Validate redirect URLs against an allowlist. - Use `bypassSecurityTrustHtml` only after an explicit security review.
- Check redirect URLs against an allowlist.
- Protect auth flows against XSS and CSRF risks. - Protect auth flows against XSS and CSRF risks.
- Store tokens only with a deliberate security concept. - Store tokens only with a deliberate security concept.
## Performance ## Performance
- Use lazy loading for larger features. - Lazy-load larger features.
- Use `OnPush` to reduce unnecessary change detection. - Use `OnPush` to reduce unnecessary change detection.
- Use `track` in lists. - Always render lists with stable `track` values.
- Use virtual scrolling for large lists. - Consider virtual scrolling for large lists.
- Avoid expensive computations in templates. - Do not run expensive calculations in templates.
- Move expensive computations into computed signals, memoization, or services. - Move expensive calculations into computed Signals, memoization, or services.
- Avoid large dependencies for small helper functions. - Do not add a large dependency for a small helper function.
- Check bundle size impact before adding libraries. - Check value, risk, and bundle cost before adding a dependency.
- Optimize images and assets. - Optimize images and assets.
- Measure performance issues before adding complex optimizations. - Measure performance problems before implementing complex optimizations.
## Testing ## Tests
- Test services that contain business logic. - Keep tests next to the code under test with `*.spec.ts`.
- Test services that contain domain logic.
- Test complex component logic. - Test complex component logic.
- Test critical user flows with E2E tests where appropriate. - Cover critical user flows with E2E tests when an E2E setup exists or is
requested.
- Mock external dependencies in unit tests. - Mock external dependencies in unit tests.
- Avoid real HTTP calls in unit tests. - Do not use real HTTP calls in unit tests.
- Write tests with clear behavioral assertions. - Write tests with a clear domain-level assertion.
- Cover edge cases and error paths. - Test edge cases and failure cases.
- Keep tests independent from internal implementation details. - Do not couple tests unnecessarily to implementation details.
- Use test names that describe behavior. - Test names should describe behavior rather than implementation.
- Do not write tests only for coverage.
- Do not use snapshots as a substitute for concrete assertions.
## Code Quality ## Code Quality and Project Practice
- Use ESLint and Prettier.
- Keep naming consistent.
- Remove unused code and unused imports. - Remove unused code and unused imports.
- Do not leave commented-out code blocks. - Do not leave commented-out code blocks in the repository.
- Keep changes focused. - Leave TODOs only with context, a ticket, or a documented decision.
- Write commits with concrete messages. - Use comments sparingly and only for context that is not obvious from the code.
- Use comments only for context that is not obvious from the code.
- Keep files small enough to understand quickly. - Keep files small enough to understand quickly.
- Follow existing project conventions. - Follow existing project conventions.
- Do not leave TODOs without context, a ticket, or a documented decision. - Prefer small, focused pull requests.
- Do not add dependencies without checking value, risk, and bundle cost. - Write commit messages with a concrete statement, for example `fix:` or
`improve:` followed by a short summary.
- Do not write commits named only `fix`, `stuff`, `update`, or `changes`.
- Document workarounds when they are unavoidable.
+15 -10
View File
@@ -1,3 +1,6 @@
ARG VCS_REF=unknown
ARG APP_VERSION=unknown
FROM node:24.14.0-bookworm-slim AS build FROM node:24.14.0-bookworm-slim AS build
WORKDIR /app WORKDIR /app
@@ -9,24 +12,26 @@ RUN npm ci --no-audit --no-fund
COPY . . COPY . .
RUN npm run build RUN npm run build
FROM node:24.14.0-bookworm-slim AS prod-deps
WORKDIR /app
ENV HUSKY=0
COPY package.json package-lock.json ./
RUN npm ci --omit=dev --ignore-scripts --no-audit --no-fund
FROM node:24.14.0-bookworm-slim AS runtime FROM node:24.14.0-bookworm-slim AS runtime
ARG VCS_REF
ARG APP_VERSION
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
COPY --from=prod-deps --chown=node:node /app/node_modules ./node_modules LABEL org.opencontainers.image.title="Personal Portfolio Website"
LABEL org.opencontainers.image.description="👋 My portfolio website built with Angular v21 — a concise introduction to who I am and what I build."
LABEL org.opencontainers.image.licenses="GPL-3.0-only"
LABEL org.opencontainers.image.version="${APP_VERSION}"
LABEL org.opencontainers.image.revision="${VCS_REF}"
LABEL org.opencontainers.image.source="https://github.com/FrauJulian/Personal-Portfolio-Website"
COPY --from=build --chown=node:node /app/dist ./dist COPY --from=build --chown=node:node /app/dist ./dist
COPY --chown=node:node scripts/static-server.mjs ./scripts/static-server.mjs
USER node USER node
EXPOSE 3000 EXPOSE 3000
CMD ["node", "dist/server/server.mjs"] CMD ["node", "scripts/static-server.mjs"]
+577 -420
View File
File diff suppressed because it is too large Load Diff
+7 -9
View File
@@ -1,16 +1,14 @@
# 🖼️ FrauJulian's Portfolio Website # FrauJulian's Portfolio Website
### [🙋‍♂️ Website Preview](https://fraujulian.xyz/) ### [Website Preview](https://fraujulian.xyz/)
<img width="2551" height="1175" alt="image" src="https://github.com/user-attachments/assets/24657c0a-4f95-4573-8472-092984d6cc4e" /> <img width="2551" height="1175" alt="Portfolio website preview" src="https://github.com/user-attachments/assets/24657c0a-4f95-4573-8472-092984d6cc4e" />
## 👂 Languages / Framework / Packages ## 👂 Languages / Framework / Packages
- Angular v21 (with SSR) - Angular v21
- TypeScript - TypeScript
- SCSS - SCSS
- Angular Material
- FontAwesome
## 💻 Development ## 💻 Development
@@ -61,8 +59,8 @@ npm run test
## 🤝 Enjoy? ## 🤝 Enjoy?
~ made by [**FrauJulian**](https://fraujulian.xyz/). ~ made by [**FrauJulian**](https://fraujulian.xyz/).
Give it a star ⭐ on
[GitHub](https://github.com/FrauJulian/Personal-Portfolio-Website)! Give it a star ⭐ on [GitHub](https://github.com/FrauJulian/Personal-Portfolio-Website)!
### Greetings from Austria! ⛰️ ### Greetings from Austria! ⛰️
+36 -10
View File
@@ -17,12 +17,29 @@
"build": { "build": {
"builder": "@angular-devkit/build-angular:application", "builder": "@angular-devkit/build-angular:application",
"options": { "options": {
"outputPath": "dist", "outputPath": {
"index": "src/index.html", "base": "dist",
"browser": "browser",
"media": "media"
},
"outputMode": "static",
"index": {
"input": "src/index.html",
"output": "index.html",
"preloadInitial": true
},
"browser": "src/main.ts", "browser": "src/main.ts",
"server": false,
"polyfills": [], "polyfills": [],
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss", "inlineStyleLanguage": "scss",
"stylePreprocessorOptions": {
"includePaths": ["src"]
},
"security": {
"autoCsp": true
},
"clearScreen": false,
"assets": [ "assets": [
{ {
"glob": "**/*", "glob": "**/*",
@@ -36,11 +53,7 @@
], ],
"styles": ["src/styles.scss"], "styles": ["src/styles.scss"],
"scripts": [], "scripts": [],
"server": "src/main.server.ts", "serviceWorker": false
"prerender": true,
"ssr": {
"entry": "src/server.ts"
}
}, },
"configurations": { "configurations": {
"production": { "production": {
@@ -48,13 +61,16 @@
"scripts": true, "scripts": true,
"styles": { "styles": {
"minify": true, "minify": true,
"inlineCritical": true "inlineCritical": true,
"removeSpecialComments": true
}, },
"fonts": { "fonts": {
"inline": true "inline": true
} }
}, },
"outputHashing": "all", "outputHashing": "all",
"crossOrigin": "anonymous",
"subresourceIntegrity": true,
"sourceMap": false, "sourceMap": false,
"namedChunks": false, "namedChunks": false,
"extractLicenses": true, "extractLicenses": true,
@@ -69,13 +85,23 @@
"type": "anyComponentStyle", "type": "anyComponentStyle",
"maximumWarning": "4kB", "maximumWarning": "4kB",
"maximumError": "8kB" "maximumError": "8kB"
},
{
"type": "anyScript",
"maximumWarning": "350kB",
"maximumError": "500kB"
} }
] ]
}, },
"development": { "development": {
"optimization": false, "optimization": false,
"extractLicenses": false, "extractLicenses": false,
"sourceMap": true, "sourceMap": {
"scripts": true,
"styles": true,
"vendor": false,
"sourcesContent": true
},
"namedChunks": true "namedChunks": true
} }
}, },
@@ -84,7 +110,7 @@
"serve": { "serve": {
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",
"options": { "options": {
"port": 3000 "port": 4200
}, },
"configurations": { "configurations": {
"production": { "production": {
+2 -7
View File
@@ -1,14 +1,9 @@
services: services:
portfolio: portfolio:
image: docker.lechner-systems.at/lechnersystems/portfolio:${IMAGE_TAG} image: docker.lechner-systems.at/lechnersystems/portfolio:${IMAGE_TAG:-latest}
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped restart: unless-stopped
env_file:
- .env
dns: dns:
- 192.168.100.2 - 192.168.100.2
- 192.168.100.6 - 192.168.100.6
ports: ports:
- ${MAIN_PORT}:3000 - '${MAIN_PORT:-3000}:3000'
+15 -11
View File
@@ -1,12 +1,16 @@
Create a new custom location: # Nginx Proxy Manager Notes
Location: ~* ## Custom Location
Path: \.(?:js|css|mjs|webp|avif|png|jpg|jpeg|gif|svg|ico|woff2?)$
Scheme/Hostname/Port: same as Origin
Extra Options:
--- Create a new custom location with these values:
- Location: `~*`
- Path: `\.(?:js|css|mjs|webp|avif|png|jpg|jpeg|gif|svg|ico|woff2?)$`
- Scheme/Hostname/Port: `same as Origin`
Use the following extra options:
```nginx
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Connection ""; proxy_set_header Connection "";
proxy_set_header Host $host; proxy_set_header Host $host;
@@ -17,13 +21,13 @@ proxy_buffering on;
expires 1y; expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable" always; add_header Cache-Control "public, max-age=31536000, immutable" always;
add_header Vary "Accept-Encoding" always; add_header Vary "Accept-Encoding" always;
```
--- ## General Configuration
General Configuration: Use the following general proxy configuration:
---
```nginx
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Connection ""; proxy_set_header Connection "";
proxy_set_header Host $host; proxy_set_header Host $host;
@@ -57,7 +61,6 @@ gzip_types
text/plain text/plain
text/xml; text/xml;
# Nur aktiv lassen, wenn dein NPMplus-Image brotli unterstützt.
brotli on; brotli on;
brotli_comp_level 5; brotli_comp_level 5;
brotli_min_length 1024; brotli_min_length 1024;
@@ -75,3 +78,4 @@ brotli_types
add_header Cache-Control "no-cache, must-revalidate" always; add_header Cache-Control "no-cache, must-revalidate" always;
add_header Vary "Accept-Encoding" always; add_header Vary "Accept-Encoding" always;
```
+3 -2
View File
@@ -26,12 +26,13 @@
"------ BUILD": "", "------ BUILD": "",
"generate:static-files": "node scripts/generate-static-files.cjs", "generate:static-files": "node scripts/generate-static-files.cjs",
"generate:optimized-assets": "node scripts/generate-optimized-assets.cjs", "generate:optimized-assets": "node scripts/generate-optimized-assets.cjs",
"generate:assets": "npm run generate:static-files && npm run generate:optimized-assets", "generate:openpgp-assets": "node scripts/generate-openpgp-assets.cjs",
"generate:assets": "npm run generate:static-files && npm run generate:optimized-assets && npm run generate:openpgp-assets",
"build": "npm run generate:assets && ng build --configuration production && node scripts/copy-static-root-files.cjs", "build": "npm run generate:assets && ng build --configuration production && node scripts/copy-static-root-files.cjs",
"------ STARTUP": "", "------ STARTUP": "",
"test": "npm run generate:assets && ng test", "test": "npm run generate:assets && ng test",
"dev": "npm run generate:assets && ng serve", "dev": "npm run generate:assets && ng serve",
"start": "node dist/server/server.mjs", "start": "node scripts/static-server.mjs",
"------ CONFIGS": "", "------ CONFIGS": "",
"prepare": "husky" "prepare": "husky"
}, },
+180
View File
@@ -0,0 +1,180 @@
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
const workspaceRoot = process.cwd();
const sourceKeyPath = path.join(workspaceRoot, 'src', 'assets', 'unoptimized', 'public.asc');
const publicKeyOutputPath = path.join(workspaceRoot, 'public', 'keys', 'public.asc');
const generatedModulePath = path.join(
workspaceRoot,
'src',
'app',
'generated',
'openpgp-key.generated.ts',
);
function ensureDirectory(directoryPath) {
fs.mkdirSync(directoryPath, { recursive: true });
}
function readAsciiArmoredKey(filePath) {
if (!fs.existsSync(filePath)) {
throw new Error(`Missing OpenPGP public key file at ${filePath}`);
}
return fs.readFileSync(filePath, 'utf8');
}
function decodeArmoredKey(armoredKey) {
const base64Lines = armoredKey
.split(/\r?\n/u)
.map((line) => line.trim())
.filter(
(line) =>
line.length > 0 &&
!line.startsWith('-----BEGIN ') &&
!line.startsWith('-----END ') &&
!line.includes(':') &&
!line.startsWith('='),
);
if (base64Lines.length === 0) {
throw new Error('OpenPGP public key armor did not contain any base64 payload.');
}
return Buffer.from(base64Lines.join(''), 'base64');
}
function parsePacketLength(buffer, offset, newFormat, lengthType) {
if (newFormat) {
const firstLengthOctet = buffer[offset];
if (firstLengthOctet < 192) {
return { headerLength: 1, packetLength: firstLengthOctet };
}
if (firstLengthOctet < 224) {
const secondLengthOctet = buffer[offset + 1];
const packetLength = ((firstLengthOctet - 192) << 8) + secondLengthOctet + 192;
return { headerLength: 2, packetLength };
}
if (firstLengthOctet === 255) {
return {
headerLength: 5,
packetLength: buffer.readUInt32BE(offset + 1),
};
}
throw new Error('OpenPGP partial body lengths are not supported by this generator.');
}
if (lengthType === 0) {
return { headerLength: 1, packetLength: buffer[offset] };
}
if (lengthType === 1) {
return { headerLength: 2, packetLength: buffer.readUInt16BE(offset) };
}
if (lengthType === 2) {
return { headerLength: 4, packetLength: buffer.readUInt32BE(offset) };
}
return { headerLength: 0, packetLength: buffer.length - offset };
}
function readPackets(buffer) {
const packets = [];
let offset = 0;
while (offset < buffer.length) {
const headerOctet = buffer[offset];
if ((headerOctet & 0x80) === 0) {
throw new Error(`Invalid OpenPGP packet header at byte offset ${offset}.`);
}
offset += 1;
const newFormat = (headerOctet & 0x40) !== 0;
const tag = newFormat ? headerOctet & 0x3f : (headerOctet >> 2) & 0x0f;
const lengthType = headerOctet & 0x03;
const { headerLength, packetLength } = parsePacketLength(buffer, offset, newFormat, lengthType);
offset += headerLength;
const body = buffer.subarray(offset, offset + packetLength);
packets.push({ tag, body });
offset += packetLength;
}
return packets;
}
function calculateFingerprintFromPacket(publicKeyPacketBody) {
const version = publicKeyPacketBody[0];
if (version === 4) {
const lengthBytes = Buffer.alloc(2);
lengthBytes.writeUInt16BE(publicKeyPacketBody.length, 0);
return crypto
.createHash('sha1')
.update(Buffer.concat([Buffer.from([0x99]), lengthBytes, publicKeyPacketBody]))
.digest('hex')
.toUpperCase();
}
if (version === 5 || version === 6) {
const lengthBytes = Buffer.alloc(4);
lengthBytes.writeUInt32BE(publicKeyPacketBody.length, 0);
return crypto
.createHash('sha256')
.update(Buffer.concat([Buffer.from([0x9a]), lengthBytes, publicKeyPacketBody]))
.digest('hex')
.toUpperCase();
}
throw new Error(`Unsupported OpenPGP public key version ${version}.`);
}
function formatFingerprint(fingerprint) {
return fingerprint.match(/.{1,4}/gu)?.join(' ') ?? fingerprint;
}
function buildGeneratedModule(assetPath, fingerprint) {
return [
'export const openPgpKey = {',
` assetPath: '${assetPath}',`,
` fingerprint: '${fingerprint}',`,
` formattedFingerprint: '${formatFingerprint(fingerprint)}',`,
'} as const;',
'',
].join('\n');
}
function run() {
const armoredKey = readAsciiArmoredKey(sourceKeyPath);
const binaryKey = decodeArmoredKey(armoredKey);
const packets = readPackets(binaryKey);
const publicKeyPacket = packets.find((packet) => packet.tag === 6);
if (!publicKeyPacket) {
throw new Error('Could not find a public-key packet in the provided OpenPGP key.');
}
const fingerprint = calculateFingerprintFromPacket(publicKeyPacket.body);
ensureDirectory(path.dirname(publicKeyOutputPath));
ensureDirectory(path.dirname(generatedModulePath));
fs.copyFileSync(sourceKeyPath, publicKeyOutputPath);
fs.writeFileSync(
generatedModulePath,
buildGeneratedModule('/keys/public.asc', fingerprint),
'utf8',
);
console.log(`Copied ${path.relative(workspaceRoot, publicKeyOutputPath)}`);
console.log(`Generated ${path.relative(workspaceRoot, generatedModulePath)}`);
console.log(`OpenPGP fingerprint: ${formatFingerprint(fingerprint)}`);
}
run();
+114
View File
@@ -0,0 +1,114 @@
import { createReadStream } from 'node:fs';
import { access, stat } from 'node:fs/promises';
import { createServer } from 'node:http';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const appRoot = path.resolve(__dirname, '..');
const distRoot = path.join(appRoot, 'dist');
const browserRoot = path.join(distRoot, 'browser');
const port = 3000;
const mimeTypes = new Map([
['.css', 'text/css; charset=utf-8'],
['.html', 'text/html; charset=utf-8'],
['.ico', 'image/x-icon'],
['.js', 'text/javascript; charset=utf-8'],
['.json', 'application/json; charset=utf-8'],
['.map', 'application/json; charset=utf-8'],
['.png', 'image/png'],
['.svg', 'image/svg+xml'],
['.txt', 'text/plain; charset=utf-8'],
['.webp', 'image/webp'],
['.xml', 'application/xml; charset=utf-8'],
]);
function getContentType(filePath) {
return mimeTypes.get(path.extname(filePath).toLowerCase()) ?? 'application/octet-stream';
}
function getCacheControl(filePath) {
const normalizedPath = filePath.replace(/\\/g, '/');
const fileName = path.basename(filePath);
const extension = path.extname(fileName).toLowerCase();
if (extension === '.html') {
return 'no-cache, must-revalidate';
}
if (/\-[A-Z0-9]{8,}\.(css|js)$/i.test(fileName)) {
return 'public, max-age=31536000, immutable';
}
if (
['.css', '.ico', '.js', '.json', '.map', '.png', '.svg', '.txt', '.webp', '.xml'].includes(
extension,
)
) {
return normalizedPath.includes('/browser/')
? 'public, max-age=604800, stale-while-revalidate=86400'
: 'public, max-age=86400, stale-while-revalidate=3600';
}
return 'public, max-age=3600';
}
async function pathExists(targetPath) {
try {
await access(targetPath);
return true;
} catch {
return false;
}
}
async function resolveFile(urlPath) {
const normalized = path.posix.normalize(decodeURIComponent(urlPath));
const relativePath = normalized.replace(/^(\.\.(\/|\\|$))+/, '').replace(/^\/+/, '');
const rootCandidate = path.join(distRoot, relativePath);
if ((await pathExists(rootCandidate)) && (await stat(rootCandidate)).isFile()) {
return rootCandidate;
}
const browserCandidate = path.join(browserRoot, relativePath);
if ((await pathExists(browserCandidate)) && (await stat(browserCandidate)).isFile()) {
return browserCandidate;
}
if (!path.posix.extname(normalized)) {
return path.join(browserRoot, 'index.html');
}
return null;
}
const server = createServer(async (request, response) => {
try {
const requestUrl = new URL(request.url ?? '/', `http://${request.headers.host ?? 'localhost'}`);
const filePath = await resolveFile(requestUrl.pathname);
if (!filePath) {
response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
response.end('Not found');
return;
}
response.writeHead(200, {
'Cache-Control': getCacheControl(filePath),
'Content-Type': getContentType(filePath),
});
createReadStream(filePath).pipe(response);
} catch (error) {
response.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
response.end('Internal server error');
console.error(error);
}
});
server.listen(port, '0.0.0.0', () => {
console.log(`Serving static app on port ${port}`);
});
-11
View File
@@ -1,11 +0,0 @@
import { provideServerRendering } from '@angular/ssr';
import type { ApplicationConfig } from '@angular/core';
import { mergeApplicationConfig } from '@angular/core';
import { appConfig } from './app.config';
const serverConfig: ApplicationConfig = {
providers: [provideServerRendering()],
};
export const config: ApplicationConfig = mergeApplicationConfig(appConfig, serverConfig);
@@ -0,0 +1,5 @@
export const openPgpKey = {
assetPath: '/keys/public.asc',
fingerprint: '98FA30F85F2030A70F45AAEB3CDEA2DE5424B8E7',
formattedFingerprint: '98FA 30F8 5F20 30A7 0F45 AAEB 3CDE A2DE 5424 B8E7',
} as const;
+16
View File
@@ -75,6 +75,22 @@
</div> </div>
</section> </section>
<section class="panel openpgp-section">
<div class="openpgp-copy">
<p class="eyebrow">{{ content().home.openPgpTitle }}</p>
<p class="openpgp-description">{{ content().home.openPgpDescription }}</p>
</div>
<div class="openpgp-actions">
<a class="action-link openpgp-download-link" [href]="openPgpKey.assetPath" download="julian-lechner-public.asc">
{{ content().home.openPgpDownload }}
</a>
<p class="openpgp-fingerprint">
<span>{{ content().home.openPgpFingerprintLabel }}</span>
<strong>{{ openPgpKey.formattedFingerprint }}</strong>
</p>
</div>
</section>
<section id="about" class="story-section panel about-scroll-anim-enabled"> <section id="about" class="story-section panel about-scroll-anim-enabled">
<div class="section-heading"> <div class="section-heading">
<p class="eyebrow">{{ content().home.aboutEyebrow }}</p> <p class="eyebrow">{{ content().home.aboutEyebrow }}</p>
+2
View File
@@ -12,6 +12,7 @@ import { interval } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { global } from '../../global'; import { global } from '../../global';
import { openPgpKey } from '../generated/openpgp-key.generated';
import type { import type {
LanguageBioTextEntry, LanguageBioTextEntry,
LanguagePortraitHighlight, LanguagePortraitHighlight,
@@ -71,6 +72,7 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
protected readonly global = global; protected readonly global = global;
protected readonly age = this.calculateAge(global.birthdate); protected readonly age = this.calculateAge(global.birthdate);
protected readonly contactMailHref = `mailto:${global.contactMail}`; protected readonly contactMailHref = `mailto:${global.contactMail}`;
protected readonly openPgpKey = openPgpKey;
protected readonly arrowRightIcon = arrowRightIcon; protected readonly arrowRightIcon = arrowRightIcon;
protected readonly arrowDownIcon = arrowDownIcon; protected readonly arrowDownIcon = arrowDownIcon;
protected readonly envelopeIcon = envelopeIcon; protected readonly envelopeIcon = envelopeIcon;
+91
View File
@@ -0,0 +1,91 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGoC8R8BEACq5XrqV728z2hYvEZCep8T3729VoFia70l7qoz1o6aDCovlIRG
PGMpcRHin0rqI2pAVHha8bMJnm34JdlPY4Dk/KI31hwLNEoTw8xzfl+58ehEnSvG
o1v3OkXC7bx4zxarFllo4/zgbiKRi1SihXZe/Ytonkm/OX0oHmwEsWlL4Gu2rX+p
Wwg/ks+l6hJ9NiB3D+F9rGfryrFgFTzV90C+1P2bOn/4NXUrr50T92Dn9zXSA1oe
k4ebnZ9df9uRlRaftNtcL/8nY7KGYr1tDBsgL2HWxknzhNoZQiFc0QJ5EjN4dz+s
kkn2aKSlzlOzBGgDeCfk6xMAyLKEewoCtagD0Fo64Mg6W5nPa8lg9xG4soGXBC2d
gUCyQPAcIw7sW71rawuFon12K+Sn02kZ0gHZVtCULh1Z66zTnw6D+fT2khbcAE85
81tuBDVrfv5WjZtSH6t7FqHOmyvLDEPDpudJKOxxuVJfjpfhhXhK17K/8148zlJH
Vmgar3Ilx/Ef0RerDD3boCXEWT0SRyFETY58iK1Vhu5hCojcn10cs/QK8X6RdQ6D
Y2WtbQ7mebqFJWyetuV1ZAFDsNnXtonODBSvhHBtqzI4IL8hFZwiM6cgM3WqfGfk
svTC5O1lePp0L/jrP0fiYjw8wv/f4wEKeM+0xOTkvXbFxlibLxZ9AS8NfQARAQAB
tCNKdWxpYW4gTGVjaG5lciA8anVsaWFuQGxlY2huZXIudG9wPokCUQQTAQgAOxYh
BJj6MPhfIDCnD0Wq6zzeot5UJLjnBQJqAvEfAhsDBQsJCAcCAiICBhUKCQgLAgQW
AgMBAh4HAheAAAoJEDzeot5UJLjnBBYP/ilO+Fmxh3P9IUWC9MHi22RQUDTVUXFY
5FZdRwcwISnZQiEO3OWcwUAPREL2LksWT18Hi+PQRsa/dHU7cePEKRVdfuBl0CU8
P0aUdJA2/zFIAy7CdKZ8gnNcAkkb3xhuKcX1UPgzWP4N2E7qOEt/Vst4JFL8cU3T
/MHQEq7iERtpLSXR/IcI+gcFKDxABYyb073W8rzt0CKAaLFjg2DXPHhFPKn5H6p4
DvXHrKfa5F5YlO+hPKcSA2L73FOmMbB8sT6TVTgPmKce8c/nWfQq3oNF775k1vJk
NBNB6Hy4cVjOOXBMxfAySWnBYaYHNzApXIyH6CUYesSWSY2wfkmlPNSstODah8Gd
YT/DK+ATmjj6JMpM7ELKZvT5iZOaau7NS7bXn9DDiwttFgM9+lxbA6hq75NTMwLF
PyLrl2Yne7y+jr9s4/v/1NxOjDwkpVyJFva4UPwR8RL93F+iomYzG3RTrjTV5j21
+zxM1hdTtbmsdh8HQi9G4XF9QpG62W0uYmTgidfQvPvUDrF041yG2bc+tSh21Xcg
EEnZuRxdKK4EtceJ20dUaG3PCy/oqg8fkGiBoHQJ3NtZDBGbYZIVXMz4mJzPOtPh
3M/7GoodKkCSJ+KedngvgAFSY6QVJCeeW0H3bGpdTYXQIYt8JwEGuUt7yU8ucEsY
NKz7VtAoYhHztCdKdWxpYW4gTGVjaG5lciA8ZnJhdWp1bGlhbkBsZWNobmVyLnRv
cD6JAlEEEwEIADsWIQSY+jD4XyAwpw9Fqus83qLeVCS45wUCagL3pgIbAwULCQgH
AgIiAgYVCgkICwIEFgIDAQIeBwIXgAAKCRA83qLeVCS45zqDD/9pHFvqCrqhF/HH
xcwh3JbXzn8sEZ1vtbrQVrXeJsGo4NNpWem3B29ZVk3P2JHTVXEiBuzkBee+cd5a
TfGTTY9c5HR1Z54j3ydFnCMUNIqqtB+WIeYNr+OU0oNHplLHZYj+MDutqP8MLZMX
X400ITTd4nz7iZmc3cgREIFGbi8QuLRhNEIn6oNkPEqumRnKweLL3TqedyAEtG+k
qsgDAMDb2LzOTWR+YLVM6g9zXmWI8fXxAC2Pda1IeTOCJd+GtldOkkbQjPLNuARi
1uLjZav92vZ/Iw75TTAQzsM9eSuRHv1JxWjUbnNORoiGEQTvyyZ7VHWiQmOLKy8q
z4OGAj2lpSgrSacqK9BD0CXi9y/DNVv1BR+akD5y77KTuF7n0d4E9dWrbr4DkLkx
8ZKyLJfQX3Ma7A0uQkCEwF7epfAuWSGDvB9wCXKUtjyCb6gRMzNOu6JKHZ9pDdO1
Hp8jGyOrs/4+Jx7RvzOeE4HlU8yv86qmjdEy7c1iJGKi3d4CQG5fVvVjZieIOhOv
RUGTgS40CPAWoKD/SIejVvSjRgt6OMS75+/ZnqGZJX7DxWlbEsIDV455Kvitw21f
WhTy8B+Hx+cbAJuRszEO1z3GGcIb61Sy4ctJZjn84wUXgJdspZtwP29a5zHfKH6x
ap9z0lNZREN6G32L5txfvbqJ9js41rQqTGVjaG5lci1TeXN0ZW1zIDxhYnVzZUBs
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
=kK3g
-----END PGP PUBLIC KEY BLOCK-----
+4
View File
@@ -18,6 +18,10 @@ export const deLanguage: LanguagePack = {
heroIntro: heroIntro:
'Ich entwickle zuverlässige Systeme, übersichtliche Benutzeroberflächen und praktische digitale Lösungen, die sich in realen Kundensystemen bewähren.', 'Ich entwickle zuverlässige Systeme, übersichtliche Benutzeroberflächen und praktische digitale Lösungen, die sich in realen Kundensystemen bewähren.',
contactTitle: 'Kontaktiere mich', contactTitle: 'Kontaktiere mich',
openPgpTitle: 'OpenPGP',
openPgpDescription: 'Verwende meinen Public Key für verschlüsselte Kommunikation.',
openPgpDownload: 'Public Key herunterladen',
openPgpFingerprintLabel: 'Fingerprint',
viewProjects: 'Projekte', viewProjects: 'Projekte',
aboutMe: 'Über mich', aboutMe: 'Über mich',
nextHighlightAriaLabel: 'Nächstes Bild anzeigen', nextHighlightAriaLabel: 'Nächstes Bild anzeigen',
+4
View File
@@ -18,6 +18,10 @@ export const enLanguage: LanguagePack = {
heroIntro: heroIntro:
'I build reliable systems, clean interfaces, and practical digital solutions that actually hold up in production.', 'I build reliable systems, clean interfaces, and practical digital solutions that actually hold up in production.',
contactTitle: 'Contact me', contactTitle: 'Contact me',
openPgpTitle: 'OpenPGP',
openPgpDescription: 'Download my public key for encrypted communication.',
openPgpDownload: 'Download public key',
openPgpFingerprintLabel: 'Fingerprint',
viewProjects: 'View projects', viewProjects: 'View projects',
aboutMe: 'About me', aboutMe: 'About me',
nextHighlightAriaLabel: 'Show next image', nextHighlightAriaLabel: 'Show next image',
+4
View File
@@ -44,6 +44,10 @@ export interface LanguagePack {
portfolioEyebrow: string; portfolioEyebrow: string;
heroIntro: string; heroIntro: string;
contactTitle: string; contactTitle: string;
openPgpTitle: string;
openPgpDescription: string;
openPgpDownload: string;
openPgpFingerprintLabel: string;
viewProjects: string; viewProjects: string;
aboutMe: string; aboutMe: string;
nextHighlightAriaLabel: string; nextHighlightAriaLabel: string;
-12
View File
@@ -1,12 +0,0 @@
import type { BootstrapContext } from '@angular/platform-browser';
import { bootstrapApplication } from '@angular/platform-browser';
import type { ApplicationRef } from '@angular/core';
import { AppComponent } from './app/app.component';
import { config } from './app/app.config.server';
const bootstrap: (context: BootstrapContext) => Promise<ApplicationRef> = (
context: BootstrapContext,
): Promise<ApplicationRef> => bootstrapApplication(AppComponent, config, context);
export default bootstrap;
-54
View File
@@ -1,54 +0,0 @@
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine, isMainModule } from '@angular/ssr/node';
import type { Express, NextFunction, Request, Response } from 'express';
import express from 'express';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import bootstrap from './main.server';
const serverDistFolder: string = dirname(fileURLToPath(import.meta.url));
const browserDistFolder: string = resolve(serverDistFolder, '../browser');
const indexHtml: string = join(serverDistFolder, 'index.server.html');
const allowedHosts: readonly string[] = [
'fraujulian.xyz',
'www.fraujulian.xyz',
'test.fraujulian.xyz',
'localhost',
'127.0.0.1',
];
const app: Express = express();
const commonEngine: CommonEngine = new CommonEngine({ allowedHosts });
app.use(
express.static(browserDistFolder, {
maxAge: '1y',
index: 'index.html',
}),
);
app.get('/{*splat}', (req: Request, res: Response, next: NextFunction): void => {
const { protocol, originalUrl, baseUrl, headers } = req;
commonEngine
.render({
bootstrap,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: browserDistFolder,
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
})
.then((html: string): Response => res.send(html))
.catch((err: Error): void => next(err));
});
if (isMainModule(import.meta.url)) {
const port: number = 3000;
app.listen(port, (): void => {
console.log(`Listening on port ${port}`);
});
}
export default app;
+68
View File
@@ -317,6 +317,61 @@ hr {
aspect-ratio: 1; aspect-ratio: 1;
} }
.openpgp-section {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: var(--space-4);
align-items: center;
margin-top: var(--space-4);
padding: 1.25rem 1.4rem;
}
.openpgp-copy,
.openpgp-actions {
min-width: 0;
}
.openpgp-copy {
display: grid;
gap: 0.45rem;
}
.openpgp-description,
.openpgp-fingerprint {
margin: 0;
}
.openpgp-description {
color: var(--text-muted);
line-height: 1.6;
}
.openpgp-actions {
display: grid;
gap: 0.7rem;
justify-items: end;
}
.openpgp-download-link {
width: 100%;
min-width: min(100%, 15rem);
}
.openpgp-fingerprint {
display: grid;
gap: 0.2rem;
text-align: right;
color: var(--text-muted);
font-size: 0.8rem;
}
.openpgp-fingerprint strong {
color: var(--text-primary);
line-height: 1.55;
letter-spacing: 0.06em;
word-break: break-word;
}
.action-link { .action-link {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -1202,6 +1257,19 @@ hr {
text-align: center; text-align: center;
} }
.openpgp-section {
grid-template-columns: 1fr;
justify-items: center;
text-align: center;
}
.openpgp-actions,
.openpgp-fingerprint {
width: 100%;
justify-items: center;
text-align: center;
}
.hero-meta div { .hero-meta div {
width: min(100%, 24rem); width: min(100%, 24rem);
} }
+5 -3
View File
@@ -1,9 +1,11 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./dist/out-tsc/app", "outDir": "./out-tsc/app",
"types": [] "types": []
}, },
"files": ["src/main.ts", "src/main.server.ts", "src/server.ts"], "include": ["src/**/*.ts"],
"include": ["src/**/*.d.ts"] "exclude": ["src/**/*.spec.ts"]
} }
+18 -11
View File
@@ -1,26 +1,33 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{ {
"compileOnSave": false, "compileOnSave": false,
"compilerOptions": { "compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true, "strict": true,
"noImplicitOverride": true, "noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true, "noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "isolatedModules": true,
"sourceMap": true, "experimentalDecorators": true,
"declaration": false, "importHelpers": true,
"moduleResolution": "bundler",
"target": "ES2022", "target": "ES2022",
"module": "ES2022", "module": "preserve"
"lib": ["ES2022", "DOM"],
"types": []
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true "strictTemplates": true
}, },
"exclude": ["node_modules"] "files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
]
} }