Landingpage vollständig, Webhook für CTA eingerichtet, erster Blogeintrag erstellt
This commit is contained in:
61
CLAUDE.md
Normal file
61
CLAUDE.md
Normal 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`
|
||||
@@ -53,8 +53,8 @@
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kB",
|
||||
"maximumError": "8kB"
|
||||
"maximumWarning": "6kB",
|
||||
"maximumError": "10kB"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
|
||||
13
public/images/text1.svg
Normal file
13
public/images/text1.svg
Normal 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 |
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -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 & 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>
|
||||
@@ -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); }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) -->
|
||||
|
||||
@@ -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
3
src/environments/n8n.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const n8nEnvironment = {
|
||||
contactWebhookUrl: 'https://n8n.hurler-webdesign.de/webhook/fbcced3d-503d-4bca-9147-405fe809253e',
|
||||
};
|
||||
Reference in New Issue
Block a user