Landingpage vollständig, Webhook für CTA eingerichtet, erster Blogeintrag erstellt

This commit is contained in:
2026-04-06 21:42:48 +02:00
parent cd694d0776
commit 11e2553549
18 changed files with 922 additions and 109 deletions

61
CLAUDE.md Normal file
View File

@@ -0,0 +1,61 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
```bash
npm start # Dev server at http://localhost:4200
npm run build # Production build (SSR)
npm test # Run unit tests with Vitest
ng generate component features/my-feature/components/my-comp # Scaffold component (SCSS, type=component)
node dist/hurler-webdesign-saas/server/server.mjs # Run SSR server after build
```
## Architecture
Angular 21 app with SSR (`@angular/ssr`) and Vitest for testing. The backend is a **Directus CMS** at `https://backend.hurler-webdesign.de` — all blog content is fetched via `BlogService` using Angular's `HttpClient`.
### Folder structure
```
src/app/
core/ # Services, models, directives — singleton, app-wide
features/ # Route-level modules (landing, blog, dashboard)
landing/ # Landingpage with Navigation, Hero, Features, Pricing, Projects, Footer
blog/ # BlogList + BlogDetail pages, fetched from Directus
shared/ # Reusable UI components (Button, NavMenu, ToggleTheme)
src/environments/ # Config constants (Directus URL, OpenPanel credentials)
src/styles/ # Global SCSS: abstracts (functions, mixins), base (tokens, typography), layout
```
### Path aliases (tsconfig)
| Alias | Resolves to |
|---|---|
| `@core/*` | `src/app/core/*` |
| `@features/*` | `src/app/features/*` |
| `@shared/*` | `src/app/shared/*` |
Routes use lazy-loaded components via `loadComponent`.
### Styling conventions
- **SCSS** everywhere; `src/styles/abstracts/` is globally included via `stylePreprocessorOptions.includePaths`
- Use `@use 'abstracts'` to access mixins/functions
- Design tokens live in `src/styles/base/_tokens.scss` as CSS custom properties (OKLCH color space, fluid typography with `clamp()`)
- Dark mode via `[data-theme="dark"]` attribute on `<html>`, managed by `ThemeService` (uses Angular Signals + `localStorage`)
- Breakpoints: `sm` (400px), `md` (700px), `lg` (1200px) — use `@include breakpoint('md')` mixin
- Max content width: 1200px — use `@mixin container-wrapper`
### Analytics
OpenPanel is configured in `src/environments/openpanel.ts` and provided app-wide via `provideOpenPanel()` in `app.config.ts`. The `OpenpanelDirective` (`@core/directives`) enables declarative event tracking.
### Angular patterns used
- Standalone components throughout (no NgModules)
- `inject()` function instead of constructor injection
- Angular Signals for reactive state (`ThemeService`, etc.)
- `provideHttpClient(withFetch())` + `provideClientHydration(withEventReplay())` for SSR hydration
- German locale (`de`) registered and set as `LOCALE_ID`

View File

@@ -53,8 +53,8 @@
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
"maximumWarning": "6kB",
"maximumError": "10kB"
}
],
"outputHashing": "all"

13
public/images/text1.svg Normal file
View File

@@ -0,0 +1,13 @@
<svg
width="244.1156"
height="183.29309"
viewBox="0 0 64.588919 48.496297"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<g>
<path
d="m 64.423989,1.7255188 q -0.05975,0.1211937 -0.896238,1.9390997 -0.836489,1.817906 -2.150973,4.6659588 -1.314483,2.8480527 -2.808214,6.0596867 -1.49373,3.211634 -2.867963,6.120284 1.135236,0.0606 1.971725,0 0.836489,-0.121194 1.254734,-0.424178 0.477994,-0.363581 0.77674,-0.363581 0.298746,0 0.298746,0.302984 0,0.424178 -0.657242,1.030147 -0.597492,0.545372 -1.792477,1.211937 -1.194984,0.605969 -2.927712,0.666566 -1.015737,2.060293 -2.389969,4.908346 -1.374233,2.787456 -2.628967,5.99909 -0.238997,0.605968 -0.477994,1.393728 -0.179247,0.787759 -0.179247,1.514921 0,0.848356 0.358495,1.454325 0.418245,0.605969 1.374232,0.605969 1.792477,0 3.584955,-1.211938 1.852226,-1.272534 3.644703,-3.211634 1.792477,-1.999696 3.465455,-4.24178 0.298747,-0.363581 0.537744,-0.363581 0.298746,0 0.298746,0.484774 0,0.363582 -0.238997,0.666566 -1.732728,2.302681 -3.584954,4.484168 -1.852227,2.12089 -3.883701,3.454022 -2.031474,1.333131 -4.361694,1.333131 -2.449718,0 -3.525204,-1.211938 -1.015737,-1.272534 -1.015737,-3.211634 0,-0.908953 0.179247,-1.878503 0.238997,-1.030146 0.537743,-2.060293 0.77674,-2.423875 1.971725,-4.847749 1.194985,-2.484472 2.031474,-4.181184 -1.015737,-0.121194 -2.808214,-0.424178 -1.792477,-0.302985 -3.8837,-0.545372 -2.091223,-0.242387 -4.182447,-0.181791 -1.493731,3.635812 -3.584954,7.514012 -2.091223,3.817602 -4.779938,7.332221 -2.628968,3.514618 -5.915176,6.302074 -3.226459,2.787456 -7.110159,4.302377 -3.8837,1.575519 -8.4246419,1.393728 -2.6289663,-0.121193 -4.839688,-1.454324 -2.2107217,-1.272535 -3.525205,-3.635812 -1.25473392,-2.363278 -1.25473392,-5.574912 0,-1.514922 0.29874618,-3.09044 0.29874617,-1.636116 0.95598774,-3.393425 1.3144833,-3.454021 3.8239511,-6.241477 2.5094679,-2.848053 5.6164278,-4.484168 3.10696,-1.696713 6.094422,-1.696713 2.628967,0 4.481193,1.75731 1.911975,1.757309 1.911975,5.090136 0,1.575519 -0.537743,3.514619 -1.075486,3.696408 -3.10696,5.696105 -2.031474,1.999697 -4.062948,2.848053 -1.493731,0.545372 -2.867963,0.545372 -1.075486,0 -1.852226,-0.363582 -0.716991,-0.363581 -0.8364898,-1.030146 v -0.181791 q 0,-0.605969 0.4779938,-0.605969 0.597492,0 0.657242,0.727163 0.05975,0.18179 0.418244,0.424178 0.418245,0.18179 1.135236,0.18179 0.477994,0 1.015737,-0.121193 0.597492,-0.121194 1.194984,-0.363581 1.553481,-0.666566 3.16671,-2.545069 1.672978,-1.878503 2.628966,-5.21133 0.597493,-1.999697 0.597493,-3.757006 0,-2.787456 -1.374233,-4.181184 -1.374232,-1.393728 -3.226458,-1.393728 -2.808215,0 -5.317682,1.757309 -2.449719,1.757309 -4.3019452,4.484168 -1.7924771,2.726859 -2.8679633,5.696106 -0.6572416,1.817906 -0.9559878,3.514618 -0.2987461,1.696712 -0.2987461,3.211634 0,3.999393 1.9717247,6.423268 1.9717247,2.423875 5.3774307,2.423875 2.987462,0 5.795676,-1.454325 2.808214,-1.393728 5.377431,-3.757006 2.569217,-2.302681 4.779939,-5.090137 2.270471,-2.848052 4.062949,-5.696105 1.852227,-2.90865 3.226459,-5.393121 1.374232,-2.484472 2.210722,-4.120587 -2.628967,0.424178 -4.540942,1.15134 -1.911977,0.666566 -3.047212,3.151037 -0.119499,0.302985 -0.537744,0.666566 -0.358495,0.302984 -0.657241,0.363581 -0.05975,0 -0.119499,0.0606 0,0 -0.05975,0 -0.418244,0 -0.418244,-0.484775 0,-0.424178 0.477993,-1.393728 1.792477,-3.029843 4.540943,-4.484168 2.748465,-1.514922 5.795676,-1.9391 1.015737,-1.9391 2.091223,-4.120587 1.075487,-2.181487 2.38997,-4.362974 1.194984,-2.120891 2.569217,-4.2417813 1.374232,-2.1814872 2.748465,-3.8176026 -0.597493,-0.060597 -1.194985,-0.060597 -0.597492,-0.060597 -1.254734,-0.060597 -3.166709,0 -6.333419,0.7877593 -3.10696,0.7877593 -5.43718,2.6056653 -2.270471,1.7573091 -2.987463,4.8477493 -0.05975,0.242388 -0.119499,0.545372 0,0.242388 0,0.545372 0,1.514922 0.89624,2.484472 0.955988,0.96955 2.270471,0.96955 1.194985,0 2.210722,-0.908953 1.075486,-0.908953 1.732728,-2.181488 0.657241,-1.272534 0.657241,-2.2420843 0,-0.7271624 0.418245,-0.7271624 0.358495,0 0.358495,0.6059687 0,2.060294 -0.955988,3.454022 -0.896238,1.333131 -2.27047,1.999697 -1.374233,0.666565 -2.867964,0.666565 -2.031474,0 -3.644704,-1.333131 -1.55348,-1.333131 -1.254734,-3.757006 0.358495,-2.848053 2.150972,-4.665959 1.852228,-1.817906 4.421445,-2.7874559 2.569217,-1.0301467 5.377431,-1.3937279 2.808214,-0.3635812 5.138434,-0.3635812 1.314483,0 2.38997,0.1211937 1.075486,0.060597 1.672978,0.1817906 0.179248,0.060597 0.179248,0.3029844 0,0.3029843 -0.298746,0.7877592 -0.238997,0.484775 -0.358496,0.6059687 -2.210721,3.6964089 -3.823951,7.5140114 -1.613229,3.817603 -3.345957,7.816997 1.732728,0.0606 3.823951,0.424178 2.150973,0.302984 4.062948,0.666565 1.911976,0.302984 2.987462,0.484775 1.971724,-3.635812 3.823951,-6.96864 1.852226,-3.393424 3.345957,-6.1202838 1.493731,-2.726859 2.389969,-4.3629744 0.83649,-1.5149217 1.55348,-2.18148716 0.716991,-0.7271624 1.075487,-0.78775927 h 0.238997 q 0.358495,0 0.477993,0.30298433 0.179248,0.24238747 0.179248,0.5453718 0,0.4847749 -0.179248,0.8483561 z"
class="logo_h"
aria-label="H" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -3,10 +3,18 @@ import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{
path: '',
renderMode: RenderMode.Prerender
renderMode: RenderMode.Prerender,
},
{
path: 'blog',
renderMode: RenderMode.Prerender,
},
{
path: 'blog/:slug',
renderMode: RenderMode.Server,
},
{
path: '**',
renderMode: RenderMode.Prerender
}
renderMode: RenderMode.Server,
},
];

View File

@@ -24,7 +24,8 @@ export class BlogService {
getPosts(): Observable<BlogPost[]> {
const params = new HttpParams()
.set('sort', '-published_at')
.set('fields', this.defaultFields);
.set('fields', this.defaultFields)
.set('filter', JSON.stringify({ status: { _eq: 'published' } }));
return this.http
.get<DirectusResponse<BlogPost[]>>(`${this.baseUrl}/items/blog_posts`, { params })

View File

@@ -32,7 +32,7 @@ export class NavigationService {
// Gefiltert nach Kontext (Landingpage vs. App)
readonly landingNavigation = computed(() =>
this._navigationItems().filter(item =>
isAnchor(item) || item.target === '/blog' || item.target === '/login'
isAnchor(item) || item.target === '/blog'
)
);

View File

@@ -0,0 +1,145 @@
<section class="contact" id="contact">
<div class="contact__wrapper">
<div class="contact__header">
<h2>Projekt anfragen</h2>
<p class="text-muted">
Erzählen Sie uns von Ihrem Vorhaben kostenlos und unverbindlich.
Wir melden uns innerhalb von 24 Stunden.
</p>
</div>
<div class="contact__grid">
<!-- Info column -->
<div class="contact__info">
<div class="contact__info-item">
<div class="contact__info-icon">
<ng-icon name="cssPin"></ng-icon>
</div>
<div class="contact__info-text">
<p class="contact__info-label">Standort</p>
<address>
Hurler Webdesign<br />
Untermagerbein 30<br />
86751 Mönchsdeggingen, Bayern
</address>
</div>
</div>
<div class="contact__info-item">
<div class="contact__info-icon">
<ng-icon name="cssAlarm"></ng-icon>
</div>
<div class="contact__info-text">
<p class="contact__info-label">Reaktionszeit</p>
<p>Antwort innerhalb von 24 Stunden</p>
</div>
</div>
<div class="contact__info-item">
<div class="contact__info-icon">
<ng-icon name="cssLock"></ng-icon>
</div>
<div class="contact__info-text">
<p class="contact__info-label">Datenschutz</p>
<p>DSGVO-konform &amp; Server in Deutschland</p>
</div>
</div>
<ul class="contact__benefits">
<li>
<ng-icon name="cssCheck" aria-hidden="true"></ng-icon>
Kostenlose Erstberatung
</li>
<li>
<ng-icon name="cssCheck" aria-hidden="true"></ng-icon>
Transparente Festpreise
</li>
<li>
<ng-icon name="cssCheck" aria-hidden="true"></ng-icon>
Persönliche Betreuung
</li>
<li>
<ng-icon name="cssCheck" aria-hidden="true"></ng-icon>
Ohne Abo oder versteckte Kosten
</li>
</ul>
</div>
<!-- Form column -->
<div class="contact__form-wrapper">
@if (!submitted()) {
<form
[formGroup]="form"
(ngSubmit)="onSubmit()"
class="contact__form"
novalidate>
<div class="contact__field" [class.contact__field--error]="nameInvalid">
<label for="contact-name">Name</label>
<input
id="contact-name"
type="text"
formControlName="name"
placeholder="Max Mustermann"
autocomplete="name"
(focus)="onFirstFocus()" />
@if (nameInvalid) {
<span class="contact__error" role="alert">Bitte geben Sie Ihren Namen ein.</span>
}
</div>
<div class="contact__field" [class.contact__field--error]="emailInvalid">
<label for="contact-email">E-Mail</label>
<input
id="contact-email"
type="email"
formControlName="email"
placeholder="max@beispiel.de"
autocomplete="email"
(focus)="onFirstFocus()" />
@if (emailInvalid) {
<span class="contact__error" role="alert">Bitte geben Sie eine gültige E-Mail-Adresse ein.</span>
}
</div>
<div class="contact__field" [class.contact__field--error]="messageInvalid">
<label for="contact-message">Nachricht</label>
<textarea
id="contact-message"
formControlName="message"
rows="5"
placeholder="Erzählen Sie uns von Ihrem Projekt was brauchen Sie, wann soll es fertig sein?"
(focus)="onFirstFocus()"></textarea>
@if (messageInvalid) {
<span class="contact__error" role="alert">Bitte schreiben Sie mindestens 20 Zeichen.</span>
}
</div>
@if (sendError()) {
<p class="contact__send-error" role="alert">
Beim Senden ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.
</p>
}
<button type="submit" class="contact__submit" [disabled]="sending()">
{{ sending() ? 'Wird gesendet…' : 'Anfrage senden' }}
</button>
</form>
} @else {
<div class="contact__success">
<div class="contact__success-icon">
<ng-icon name="cssCheck"></ng-icon>
</div>
<h3>Vielen Dank für Ihre Anfrage!</h3>
<p>Wir melden uns innerhalb von 24 Stunden per E-Mail bei Ihnen.</p>
<button class="contact__reset" (click)="resetForm()">
Weitere Anfrage senden
</button>
</div>
}
</div>
</div>
</div>
</section>

View File

@@ -0,0 +1,255 @@
@use 'abstracts';
// ── Animations ────────────────────────────────────────────────────────────────
@keyframes success-pop {
from { opacity: 0; transform: scale(0.95) translateY(8px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
// ── Section ───────────────────────────────────────────────────────────────────
.contact {
padding-block: calc(var(--space-4) * 2);
background-color: var(--bg-muted);
&__wrapper {
@include abstracts.container-wrapper;
}
// ── Header ─────────────────────────────────────────────────────────────────
&__header {
text-align: center;
margin-bottom: calc(var(--space-4) * 1.5);
h2 {
font-size: var(--font-size-xl);
margin-bottom: var(--space-2);
}
p {
font-size: var(--font-size-lg);
max-width: 56ch;
margin-inline: auto;
}
}
// ── Two-column grid ─────────────────────────────────────────────────────────
&__grid {
display: grid;
grid-template-columns: 1fr;
gap: calc(var(--space-4) * 1.5);
@include abstracts.breakpoint('md') {
grid-template-columns: 1fr 1.4fr;
align-items: start;
}
}
// ── Info column ────────────────────────────────────────────────────────────
&__info {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
&__info-item {
display: flex;
gap: var(--space-3);
align-items: flex-start;
}
&__info-icon {
display: flex;
align-items: center;
justify-content: center;
width: abstracts.rem(40);
height: abstracts.rem(40);
border-radius: 10px;
background-color: oklch(from var(--accent) l c h / 0.12);
color: var(--accent);
font-size: abstracts.rem(18);
flex-shrink: 0;
}
&__info-text {
p, address {
font-size: var(--font-size-base);
color: var(--text-muted);
font-style: normal;
line-height: 1.6;
}
.contact__info-label {
font-weight: 700;
color: var(--text-main);
margin-bottom: var(--space-1);
}
}
&__benefits {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: var(--space-2);
padding-top: var(--space-2);
border-top: 1px solid var(--border-color);
li {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--font-size-base);
color: var(--text-muted);
ng-icon {
color: var(--accent);
font-size: abstracts.rem(16);
flex-shrink: 0;
}
}
}
// ── Form wrapper ───────────────────────────────────────────────────────────
&__form-wrapper {
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: var(--space-4);
}
&__form {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
// ── Field ──────────────────────────────────────────────────────────────────
&__field {
display: flex;
flex-direction: column;
gap: var(--space-1);
label {
font-size: var(--font-size-base);
font-weight: 600;
color: var(--text-main);
}
input,
textarea {
width: 100%;
box-sizing: border-box;
padding: 10px 14px;
border: 1.5px solid var(--border-color);
border-radius: calc(var(--border-radius) * 0.6);
background-color: var(--bg-surface);
color: var(--text-main);
font-size: var(--font-size-base);
font-family: inherit;
line-height: 1.5;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
resize: vertical;
&::placeholder {
color: var(--text-muted);
opacity: 0.6;
}
&:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px oklch(from var(--accent) l c h / 0.15);
}
}
&--error {
input,
textarea {
border-color: oklch(55% 0.2 25);
&:focus {
box-shadow: 0 0 0 3px oklch(55% 0.2 25 / 0.15);
}
}
}
}
&__error {
font-size: 0.8rem;
color: oklch(55% 0.2 25);
}
// ── Submit button ──────────────────────────────────────────────────────────
&__submit {
width: 100%;
padding: 12px 24px;
background-color: var(--accent);
color: var(--text-on-accent);
border: none;
border-radius: calc(var(--border-radius) * 0.6);
font-size: var(--font-size-base);
font-weight: 600;
font-family: inherit;
cursor: pointer;
transition: background-color 0.2s ease, transform 0.15s ease;
&:hover { background-color: var(--accent-hover); }
&:active { transform: translateY(1px); }
}
// ── Success state ──────────────────────────────────────────────────────────
&__success {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: var(--space-4);
gap: var(--space-3);
animation: success-pop 0.4s cubic-bezier(0.22, 1, 0.36, 1);
h3 {
font-size: var(--font-size-lg);
font-weight: 700;
}
p {
color: var(--text-muted);
font-size: var(--font-size-base);
max-width: 36ch;
}
}
&__success-icon {
display: flex;
align-items: center;
justify-content: center;
width: abstracts.rem(64);
height: abstracts.rem(64);
border-radius: 50%;
background-color: oklch(from var(--accent) l c h / 0.12);
color: var(--accent);
font-size: abstracts.rem(28);
}
&__reset {
background: none;
border: 1.5px solid var(--border-color);
border-radius: calc(var(--border-radius) * 0.6);
padding: 8px 20px;
font-size: var(--font-size-base);
font-family: inherit;
color: var(--text-muted);
cursor: pointer;
transition: border-color 0.2s ease, color 0.2s ease;
&:hover { border-color: var(--accent); color: var(--accent); }
}
}

View File

@@ -0,0 +1,91 @@
import { Component, inject, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { cssCheck, cssPin, cssAlarm, cssLock } from '@ng-icons/css.gg';
import { OpenPanelService } from '@core/services/openpanel.service';
import { n8nEnvironment } from '../../../../../environments/n8n';
@Component({
selector: 'app-contact',
imports: [ReactiveFormsModule, NgIcon],
viewProviders: [provideIcons({ cssCheck, cssPin, cssAlarm, cssLock })],
templateUrl: './contact.component.html',
styleUrl: './contact.component.scss',
})
export class ContactComponent {
private readonly fb = inject(FormBuilder);
private readonly op = inject(OpenPanelService);
private readonly http = inject(HttpClient);
readonly submitted = signal(false);
readonly sending = signal(false);
readonly sendError = signal(false);
private hasStarted = false;
readonly form = this.fb.nonNullable.group({
name: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]],
message: ['', [Validators.required, Validators.minLength(20)]],
});
onFirstFocus(): void {
if (!this.hasStarted) {
this.hasStarted = true;
this.op.track('contact_form_start');
}
}
onSubmit(): void {
console.log('submit');
if (this.form.invalid) {
this.form.markAllAsTouched();
this.op.track('contact_form_validation_error');
return;
}
this.op.track('contact_form_submit', {
message_length: this.form.value.message?.length ?? 0,
});
this.sending.set(true);
this.sendError.set(false);
console.log('posting to', n8nEnvironment.contactWebhookUrl, 'with payload', this.form.getRawValue());
this.http
.post(n8nEnvironment.contactWebhookUrl, this.form.getRawValue())
.subscribe({
next: () => {
this.submitted.set(true);
this.form.reset();
this.hasStarted = false;
this.sending.set(false);
},
error: () => {
this.sendError.set(true);
this.sending.set(false);
this.op.track('contact_form_send_error');
},
});
}
resetForm(): void {
this.submitted.set(false);
}
get nameInvalid(): boolean {
const c = this.form.controls.name;
return c.invalid && c.touched;
}
get emailInvalid(): boolean {
const c = this.form.controls.email;
return c.invalid && c.touched;
}
get messageInvalid(): boolean {
const c = this.form.controls.message;
return c.invalid && c.touched;
}
}

View File

@@ -1,17 +1,66 @@
<div class="wrapper">
<section class="header">
<div class="logo-container">
<span class="logo-container__logo centered">H</span>
<a href="#hero" class="logo-container" (click)="closeMenu()">
<span class="logo-container__logo centered">
<svg width="244.1156" height="183.29309" viewBox="0 0 64.588919 48.496297" xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg" aria-hidden="true">
<path
d="m 64.423989,1.7255188 q -0.05975,0.1211937 -0.896238,1.9390997 -0.836489,1.817906 -2.150973,4.6659588 -1.314483,2.8480527 -2.808214,6.0596867 -1.49373,3.211634 -2.867963,6.120284 1.135236,0.0606 1.971725,0 0.836489,-0.121194 1.254734,-0.424178 0.477994,-0.363581 0.77674,-0.363581 0.298746,0 0.298746,0.302984 0,0.424178 -0.657242,1.030147 -0.597492,0.545372 -1.792477,1.211937 -1.194984,0.605969 -2.927712,0.666566 -1.015737,2.060293 -2.389969,4.908346 -1.374233,2.787456 -2.628967,5.99909 -0.238997,0.605968 -0.477994,1.393728 -0.179247,0.787759 -0.179247,1.514921 0,0.848356 0.358495,1.454325 0.418245,0.605969 1.374232,0.605969 1.792477,0 3.584955,-1.211938 1.852226,-1.272534 3.644703,-3.211634 1.792477,-1.999696 3.465455,-4.24178 0.298747,-0.363581 0.537744,-0.363581 0.298746,0 0.298746,0.484774 0,0.363582 -0.238997,0.666566 -1.732728,2.302681 -3.584954,4.484168 -1.852227,2.12089 -3.883701,3.454022 -2.031474,1.333131 -4.361694,1.333131 -2.449718,0 -3.525204,-1.211938 -1.015737,-1.272534 -1.015737,-3.211634 0,-0.908953 0.179247,-1.878503 0.238997,-1.030146 0.537743,-2.060293 0.77674,-2.423875 1.971725,-4.847749 1.194985,-2.484472 2.031474,-4.181184 -1.015737,-0.121194 -2.808214,-0.424178 -1.792477,-0.302985 -3.8837,-0.545372 -2.091223,-0.242387 -4.182447,-0.181791 -1.493731,3.635812 -3.584954,7.514012 -2.091223,3.817602 -4.779938,7.332221 -2.628968,3.514618 -5.915176,6.302074 -3.226459,2.787456 -7.110159,4.302377 -3.8837,1.575519 -8.4246419,1.393728 -2.6289663,-0.121193 -4.839688,-1.454324 -2.2107217,-1.272535 -3.525205,-3.635812 -1.25473392,-2.363278 -1.25473392,-5.574912 0,-1.514922 0.29874618,-3.09044 0.29874617,-1.636116 0.95598774,-3.393425 1.3144833,-3.454021 3.8239511,-6.241477 2.5094679,-2.848053 5.6164278,-4.484168 3.10696,-1.696713 6.094422,-1.696713 2.628967,0 4.481193,1.75731 1.911975,1.757309 1.911975,5.090136 0,1.575519 -0.537743,3.514619 -1.075486,3.696408 -3.10696,5.696105 -2.031474,1.999697 -4.062948,2.848053 -1.493731,0.545372 -2.867963,0.545372 -1.075486,0 -1.852226,-0.363582 -0.716991,-0.363581 -0.8364898,-1.030146 v -0.181791 q 0,-0.605969 0.4779938,-0.605969 0.597492,0 0.657242,0.727163 0.05975,0.18179 0.418244,0.424178 0.418245,0.18179 1.135236,0.18179 0.477994,0 1.015737,-0.121193 0.597492,-0.121194 1.194984,-0.363581 1.553481,-0.666566 3.16671,-2.545069 1.672978,-1.878503 2.628966,-5.21133 0.597493,-1.999697 0.597493,-3.757006 0,-2.787456 -1.374233,-4.181184 -1.374232,-1.393728 -3.226458,-1.393728 -2.808215,0 -5.317682,1.757309 -2.449719,1.757309 -4.3019452,4.484168 -1.7924771,2.726859 -2.8679633,5.696106 -0.6572416,1.817906 -0.9559878,3.514618 -0.2987461,1.696712 -0.2987461,3.211634 0,3.999393 1.9717247,6.423268 1.9717247,2.423875 5.3774307,2.423875 2.987462,0 5.795676,-1.454325 2.808214,-1.393728 5.377431,-3.757006 2.569217,-2.302681 4.779939,-5.090137 2.270471,-2.848052 4.062949,-5.696105 1.852227,-2.90865 3.226459,-5.393121 1.374232,-2.484472 2.210722,-4.120587 -2.628967,0.424178 -4.540942,1.15134 -1.911977,0.666566 -3.047212,3.151037 -0.119499,0.302985 -0.537744,0.666566 -0.358495,0.302984 -0.657241,0.363581 -0.05975,0 -0.119499,0.0606 0,0 -0.05975,0 -0.418244,0 -0.418244,-0.484775 0,-0.424178 0.477993,-1.393728 1.792477,-3.029843 4.540943,-4.484168 2.748465,-1.514922 5.795676,-1.9391 1.015737,-1.9391 2.091223,-4.120587 1.075487,-2.181487 2.38997,-4.362974 1.194984,-2.120891 2.569217,-4.2417813 1.374232,-2.1814872 2.748465,-3.8176026 -0.597493,-0.060597 -1.194985,-0.060597 -0.597492,-0.060597 -1.254734,-0.060597 -3.166709,0 -6.333419,0.7877593 -3.10696,0.7877593 -5.43718,2.6056653 -2.270471,1.7573091 -2.987463,4.8477493 -0.05975,0.242388 -0.119499,0.545372 0,0.242388 0,0.545372 0,1.514922 0.89624,2.484472 0.955988,0.96955 2.270471,0.96955 1.194985,0 2.210722,-0.908953 1.075486,-0.908953 1.732728,-2.181488 0.657241,-1.272534 0.657241,-2.2420843 0,-0.7271624 0.418245,-0.7271624 0.358495,0 0.358495,0.6059687 0,2.060294 -0.955988,3.454022 -0.896238,1.333131 -2.27047,1.999697 -1.374233,0.666565 -2.867964,0.666565 -2.031474,0 -3.644704,-1.333131 -1.55348,-1.333131 -1.254734,-3.757006 0.358495,-2.848053 2.150972,-4.665959 1.852228,-1.817906 4.421445,-2.7874559 2.569217,-1.0301467 5.377431,-1.3937279 2.808214,-0.3635812 5.138434,-0.3635812 1.314483,0 2.38997,0.1211937 1.075486,0.060597 1.672978,0.1817906 0.179248,0.060597 0.179248,0.3029844 0,0.3029843 -0.298746,0.7877592 -0.238997,0.484775 -0.358496,0.6059687 -2.210721,3.6964089 -3.823951,7.5140114 -1.613229,3.817603 -3.345957,7.816997 1.732728,0.0606 3.823951,0.424178 2.150973,0.302984 4.062948,0.666565 1.911976,0.302984 2.987462,0.484775 1.971724,-3.635812 3.823951,-6.96864 1.852226,-3.393424 3.345957,-6.1202838 1.493731,-2.726859 2.389969,-4.3629744 0.83649,-1.5149217 1.55348,-2.18148716 0.716991,-0.7271624 1.075487,-0.78775927 h 0.238997 q 0.358495,0 0.477993,0.30298433 0.179248,0.24238747 0.179248,0.5453718 0,0.4847749 -0.179248,0.8483561 z"
class="logo_h" aria-label="H" />
</svg>
</span>
<p class="logo-container__company"><span>Hurler</span> Webdesign</p>
</div>
</a>
<div class="header__nav-section centered">
<div class="theme-toggle-container">
<app-toogle-theme></app-toogle-theme>
</div>
<app-nav-menu [items]="navigationService.landingNavigation()"></app-nav-menu>
<div class="burger-menu centered">
<ng-icon name="cssMenu" class="burger-menu__icon"></ng-icon>
<div class="header__login-btn">
<app-button
[item]="loginItem"
variant="primary"
size="sm"
[disabled]="true">
</app-button>
</div>
<button
class="burger-menu centered"
(click)="toggleMenu()"
[attr.aria-expanded]="isMenuOpen()"
aria-label="Navigation öffnen">
<ng-icon [name]="isMenuOpen() ? 'cssClose' : 'cssMenu'" class="burger-menu__icon"></ng-icon>
</button>
</div>
</section>
</div>
<!-- Mobile Menu Overlay -->
@if (isMenuOpen()) {
<div class="mobile-overlay" (click)="closeMenu()" aria-hidden="true"></div>
<nav class="mobile-menu" aria-label="Mobile Navigation">
<ul class="mobile-menu__list">
@for (item of navigationService.landingNavigation(); track item.target) {
<li class="mobile-menu__item">
@if (isAnchor(item)) {
<a [href]="'#' + item.target" (click)="onMobileNavClick($event, item)">
{{ item.label }}
</a>
} @else {
<a [routerLink]="item.target" (click)="closeMenu()">
{{ item.label }}
</a>
}
</li>
}
</ul>
<div class="mobile-menu__cta">
<app-button
[item]="loginItem"
variant="primary"
[disabled]="true">
</app-button>
</div>
</nav>
}

View File

@@ -1,86 +1,204 @@
@use "abstracts";
.wrapper {
height: var(--nav-height);
border-radius: 0 0 10px 10px;
background-color: var(--nav-bg);
backdrop-filter: var(--nav-backdrop);
box-shadow: var(--nav-shadow);
position: sticky;
top: 0;
z-index: var(--z-index-sticky);
// ── Animations ────────────────────────────────────────────────────────────────
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slide-down {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
// ── Sticky wrapper ────────────────────────────────────────────────────────────
.wrapper {
height: var(--nav-height);
border-radius: 0 0 10px 10px;
background-color: var(--nav-bg);
backdrop-filter: var(--nav-backdrop);
box-shadow: var(--nav-shadow);
position: sticky;
top: 0;
z-index: var(--z-index-sticky);
}
// ── Header row ────────────────────────────────────────────────────────────────
.header {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
min-height: abstracts.rem(60);
position: sticky;
top: 0;
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
min-height: abstracts.rem(60);
padding-inline: var(--space-3);
&__nav-section {
display: flex;
flex-direction: row;
@include abstracts.breakpoint('md') {
padding-inline: var(--space-4);
}
&__nav-section {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--space-2);
}
&__login-btn {
display: none;
@include abstracts.breakpoint('md') {
display: flex;
align-items: center;
}
}
}
// ── Logo ──────────────────────────────────────────────────────────────────────
.logo-container {
display: flex;
align-items: center;
gap: var(--space-2);
text-decoration: none;
&__logo {
stroke: var(--text-on-accent);
fill: var(--text-on-accent);
font-size: 1.5rem;
font-weight: bold;
width: abstracts.rem(30);
height: abstracts.rem(30);
display: flex;
align-items: center;
justify-items: center;
gap: var(--space-2);
padding-left: var(--space-4);
background-color: var(--accent);
border-radius: 6px;
flex-shrink: 0;
padding: 4px;
}
&__logo {
font-size: 1.5rem;
font-weight: bold;
color: var(--text-on-accent);
border: 1px solid var(--accent);
width: abstracts.rem(30);
height: abstracts.rem(30);
display: flex;
background-color: var(--accent);
border-radius: 5px;
}
&__company {
font-size: var(--font-size-base);
font-weight: 700;
color: var(--text-main);
margin: auto;
span {
color: var(--accent);
}
&__company {
font-size: var(--font-size-base);
font-weight: 700;
color: var(--text-main);
span {
color: var(--accent);
}
}
}
// ── Theme toggle ──────────────────────────────────────────────────────────────
.theme-toggle-container {
width: abstracts.rem(24);
height: abstracts.rem(24);
margin: auto;
margin-right: var(--space-4);
@include abstracts.breakpoint("md") {
margin: auto;
margin-right: var(--space-4);
}
width: abstracts.rem(24);
height: abstracts.rem(24);
display: flex;
align-items: center;
}
// ── Burger button ─────────────────────────────────────────────────────────────
.burger-menu {
padding: 0 var(--space-4) 0 var(--space-4);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-1);
background: none;
border: none;
cursor: pointer;
border-radius: 6px;
transition: background-color 0.2s ease;
@include abstracts.breakpoint("md") {
display: none;
&:hover {
background-color: oklch(from var(--accent) l c h / 0.1);
}
@include abstracts.breakpoint('md') {
display: none;
}
&__icon {
width: abstracts.rem(24);
height: abstracts.rem(24);
color: var(--text-main);
}
}
// ── Mobile overlay ────────────────────────────────────────────────────────────
.mobile-overlay {
position: fixed;
inset: 0;
background-color: oklch(0% 0 0 / 0.4);
z-index: calc(var(--z-index-sticky) - 1);
backdrop-filter: blur(4px);
animation: fade-in 0.2s ease;
@include abstracts.breakpoint('md') {
display: none;
}
}
// ── Mobile menu ───────────────────────────────────────────────────────────────
.mobile-menu {
position: fixed;
top: var(--nav-height);
left: 0;
right: 0;
background-color: var(--bg-surface);
border-bottom: 1px solid var(--border-color);
border-radius: 0 0 var(--border-radius) var(--border-radius);
padding: var(--space-3) var(--space-4) var(--space-4);
z-index: var(--z-index-sticky);
animation: slide-down 0.25s cubic-bezier(0.22, 1, 0.36, 1);
box-shadow: 0 8px 32px oklch(0% 0 0 / 0.12);
@include abstracts.breakpoint('md') {
display: none;
}
&__list {
list-style: none;
padding: 0;
margin: 0 0 var(--space-4);
}
&__item {
a {
display: block;
padding: var(--space-3) 0;
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-main);
text-decoration: none;
border-bottom: 1px solid var(--border-color);
transition: color 0.2s ease, padding-left 0.2s ease;
&:hover {
color: var(--accent);
padding-left: var(--space-2);
}
}
&__icon {
width: abstracts.rem(32);
height: abstracts.rem(32);
color: var(--text-main);
&:last-child a {
border-bottom: none;
}
}
}
&__cta {
display: flex;
justify-content: center;
app-button {
width: 100%;
::ng-deep .nav-btn {
width: 100%;
justify-content: center;
}
}
}
}

View File

@@ -1,15 +1,53 @@
import { Component, inject } from '@angular/core';
import { NgIcon } from '@ng-icons/core';
import { Component, inject, signal, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { RouterLink } from '@angular/router';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { cssMenu, cssClose } from '@ng-icons/css.gg';
import { ToogleThemeComponent } from '@shared/utils/toogle-theme/toogle-theme.component';
import { NavMenuComponent } from '@shared/ui/nav-menu/nav-menu.component';
import { ButtonComponent } from '@shared/ui/button/button.component';
import { NavigationService } from '@core/services/navigation.service';
import { OpenPanelService } from '@core/services/openpanel.service';
import { NavigationItem, isAnchor } from '@core/models/navigation.model';
@Component({
selector: 'app-navigation',
imports: [NgIcon, ToogleThemeComponent, NavMenuComponent],
imports: [NgIcon, ToogleThemeComponent, NavMenuComponent, ButtonComponent, RouterLink],
viewProviders: [provideIcons({ cssMenu, cssClose })],
templateUrl: './navigation.component.html',
styleUrl: './navigation.component.scss',
})
export class NavigationComponent {
protected readonly navigationService = inject(NavigationService);
private readonly op = inject(OpenPanelService);
private readonly platformId = inject(PLATFORM_ID);
readonly isMenuOpen = signal(false);
protected readonly isAnchor = isAnchor;
readonly loginItem: NavigationItem = { label: 'Login', type: 'route', target: '/login' };
toggleMenu(): void {
const next = !this.isMenuOpen();
this.isMenuOpen.set(next);
if (isPlatformBrowser(this.platformId)) {
document.body.style.overflow = next ? 'hidden' : '';
}
this.op.track(next ? 'mobile_menu_open' : 'mobile_menu_close');
}
closeMenu(): void {
this.isMenuOpen.set(false);
if (isPlatformBrowser(this.platformId)) {
document.body.style.overflow = '';
}
}
onMobileNavClick(event: Event, item: NavigationItem): void {
this.closeMenu();
if (isAnchor(item)) {
event.preventDefault();
this.navigationService.navigate(item);
}
}
}

View File

@@ -40,7 +40,11 @@
border-radius: var(--border-radius);
overflow: hidden;
aspect-ratio: 4 / 3;
background-color: var(--bg-muted);
background: linear-gradient(
135deg,
oklch(from var(--accent) calc(l + 0.1) calc(c * 0.6) h),
oklch(from var(--accent) calc(l - 0.1) c h)
);
img {
width: 100%;

View File

@@ -3,4 +3,5 @@
<app-features-section></app-features-section>
<app-projects></app-projects>
<app-pricing></app-pricing>
<app-contact></app-contact>
<app-footer></app-footer>

View File

@@ -5,6 +5,7 @@ import { FeaturesSectionComponent } from '../components/features-section/feature
import { FooterComponent } from '../components/footer/footer.component';
import { ProjectsComponent } from '../components/projects/projects.component';
import { PricingComponent } from '../components/pricing/pricing.component';
import { ContactComponent } from '../components/contact/contact.component';
import { SeoService } from '@core/services/seo.service';
@Component({
@@ -15,6 +16,7 @@ import { SeoService } from '@core/services/seo.service';
FeaturesSectionComponent,
ProjectsComponent,
PricingComponent,
ContactComponent,
FooterComponent,
],
templateUrl: './landingpage.component.html',

View File

@@ -1,16 +1,30 @@
<!-- Router Link (internal navigation) -->
@if (isRouteType) {
<a
[routerLink]="item.target"
routerLinkActive="nav-btn--active"
[class]="hostClasses.join(' ')"
[attr.aria-disabled]="disabled || null"
>
@if (item.icon) {
<span class="nav-btn__icon" aria-hidden="true">{{ item.icon }}</span>
}
<span class="nav-btn__label">{{ item.label }}</span>
</a>
@if (disabled) {
<button
[class]="hostClasses.join(' ')"
disabled
type="button"
aria-disabled="true"
>
@if (item.icon) {
<span class="nav-btn__icon" aria-hidden="true">{{ item.icon }}</span>
}
<span class="nav-btn__label">{{ item.label }}</span>
<span class="nav-btn__badge">Demnächst</span>
</button>
} @else {
<a
[routerLink]="item.target"
routerLinkActive="nav-btn--active"
[class]="hostClasses.join(' ')"
>
@if (item.icon) {
<span class="nav-btn__icon" aria-hidden="true">{{ item.icon }}</span>
}
<span class="nav-btn__label">{{ item.label }}</span>
</a>
}
}
<!-- Anchor (smooth scroll) -->

View File

@@ -1,33 +1,33 @@
// ─── Design Tokens ───────────────────────────────────────────────────────────
:host {
--btn-font: 'DM Mono', 'Courier New', monospace;
--btn-radius: 4px;
--btn-font: inherit;
--btn-radius: var(--border-radius, 8px);
--btn-transition: 160ms cubic-bezier(0.4, 0, 0.2, 1);
// Size tokens
--btn-sm-padding: 6px 14px;
--btn-md-padding: 10px 22px;
--btn-sm-padding: 6px 16px;
--btn-md-padding: 10px 24px;
--btn-lg-padding: 14px 32px;
--btn-sm-font: 0.75rem;
--btn-md-font: 0.875rem;
--btn-sm-font: 0.8rem;
--btn-md-font: 0.9rem;
--btn-lg-font: 1rem;
// Color tokens — override at :root level to theme globally
--btn-primary-bg: #0f0f0f;
--btn-primary-color: #f5f5f5;
--btn-primary-border: #0f0f0f;
--btn-primary-hover-bg: #2a2a2a;
// Color tokens — use global theme CSS custom properties
--btn-primary-bg: var(--accent);
--btn-primary-color: var(--text-on-accent);
--btn-primary-border: var(--accent);
--btn-primary-hover-bg: var(--accent-hover);
--btn-ghost-bg: transparent;
--btn-ghost-color: #0f0f0f;
--btn-ghost-border: transparent;
--btn-ghost-hover-bg: rgba(0, 0, 0, 0.06);
--btn-ghost-bg: transparent;
--btn-ghost-color: var(--text-main);
--btn-ghost-border: transparent;
--btn-ghost-hover-bg: oklch(from var(--accent) l c h / 0.1);
--btn-outline-bg: transparent;
--btn-outline-color: #0f0f0f;
--btn-outline-border: #0f0f0f;
--btn-outline-hover-bg: #0f0f0f;
--btn-outline-hover-color: #f5f5f5;
--btn-outline-bg: transparent;
--btn-outline-color: var(--accent);
--btn-outline-border: var(--accent);
--btn-outline-hover-bg: var(--accent);
--btn-outline-hover-color: var(--text-on-accent);
display: inline-block;
}
@@ -136,4 +136,14 @@
opacity: 0.65;
margin-left: -2px;
}
&__badge {
font-size: 0.65em;
padding: 2px 6px;
border-radius: 999px;
background-color: oklch(from currentColor l c h / 0.15);
letter-spacing: 0;
text-transform: none;
font-weight: 500;
}
}

3
src/environments/n8n.ts Normal file
View File

@@ -0,0 +1,3 @@
export const n8nEnvironment = {
contactWebhookUrl: 'https://n8n.hurler-webdesign.de/webhook/fbcced3d-503d-4bca-9147-405fe809253e',
};