diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a5a9ade --- /dev/null +++ b/CLAUDE.md @@ -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 ``, 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` diff --git a/angular.json b/angular.json index 2f6d4a2..796b237 100644 --- a/angular.json +++ b/angular.json @@ -53,8 +53,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kB", - "maximumError": "8kB" + "maximumWarning": "6kB", + "maximumError": "10kB" } ], "outputHashing": "all" diff --git a/public/images/text1.svg b/public/images/text1.svg new file mode 100644 index 0000000..e247738 --- /dev/null +++ b/public/images/text1.svg @@ -0,0 +1,13 @@ + + + + + diff --git a/src/app/app.routes.server.ts b/src/app/app.routes.server.ts index 4c5dd72..99bd9b2 100644 --- a/src/app/app.routes.server.ts +++ b/src/app/app.routes.server.ts @@ -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, + }, ]; diff --git a/src/app/core/services/blog.service.ts b/src/app/core/services/blog.service.ts index f139653..c2df227 100644 --- a/src/app/core/services/blog.service.ts +++ b/src/app/core/services/blog.service.ts @@ -24,7 +24,8 @@ export class BlogService { getPosts(): Observable { 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>(`${this.baseUrl}/items/blog_posts`, { params }) diff --git a/src/app/core/services/navigation.service.ts b/src/app/core/services/navigation.service.ts index d79327e..e7b2a2f 100644 --- a/src/app/core/services/navigation.service.ts +++ b/src/app/core/services/navigation.service.ts @@ -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' ) ); diff --git a/src/app/features/landing/components/contact/contact.component.html b/src/app/features/landing/components/contact/contact.component.html new file mode 100644 index 0000000..102580f --- /dev/null +++ b/src/app/features/landing/components/contact/contact.component.html @@ -0,0 +1,145 @@ +
+
+
+

Projekt anfragen

+

+ Erzählen Sie uns von Ihrem Vorhaben – kostenlos und unverbindlich. + Wir melden uns innerhalb von 24 Stunden. +

+
+ +
+ + +
+
+
+ +
+
+

Standort

+
+ Hurler Webdesign
+ Untermagerbein 30
+ 86751 Mönchsdeggingen, Bayern +
+
+
+ +
+
+ +
+
+

Reaktionszeit

+

Antwort innerhalb von 24 Stunden

+
+
+ +
+
+ +
+
+

Datenschutz

+

DSGVO-konform & Server in Deutschland

+
+
+ +
    +
  • + + Kostenlose Erstberatung +
  • +
  • + + Transparente Festpreise +
  • +
  • + + Persönliche Betreuung +
  • +
  • + + Ohne Abo oder versteckte Kosten +
  • +
+
+ + +
+ @if (!submitted()) { +
+ +
+ + + @if (nameInvalid) { + Bitte geben Sie Ihren Namen ein. + } +
+ +
+ + + @if (emailInvalid) { + Bitte geben Sie eine gültige E-Mail-Adresse ein. + } +
+ +
+ + + @if (messageInvalid) { + Bitte schreiben Sie mindestens 20 Zeichen. + } +
+ + @if (sendError()) { + + } + + +
+ } @else { +
+
+ +
+

Vielen Dank für Ihre Anfrage!

+

Wir melden uns innerhalb von 24 Stunden per E-Mail bei Ihnen.

+ +
+ } +
+ +
+
+
diff --git a/src/app/features/landing/components/contact/contact.component.scss b/src/app/features/landing/components/contact/contact.component.scss new file mode 100644 index 0000000..e2fb45d --- /dev/null +++ b/src/app/features/landing/components/contact/contact.component.scss @@ -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); } + } +} diff --git a/src/app/features/landing/components/contact/contact.component.ts b/src/app/features/landing/components/contact/contact.component.ts new file mode 100644 index 0000000..95cfa4d --- /dev/null +++ b/src/app/features/landing/components/contact/contact.component.ts @@ -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; + } +} diff --git a/src/app/features/landing/components/navigation/navigation.component.html b/src/app/features/landing/components/navigation/navigation.component.html index 05a9949..35d174c 100644 --- a/src/app/features/landing/components/navigation/navigation.component.html +++ b/src/app/features/landing/components/navigation/navigation.component.html @@ -1,17 +1,66 @@
- + +
-
- + +
+ + +@if (isMenuOpen()) { + + +} diff --git a/src/app/features/landing/components/navigation/navigation.component.scss b/src/app/features/landing/components/navigation/navigation.component.scss index f286b6d..08fa0d2 100644 --- a/src/app/features/landing/components/navigation/navigation.component.scss +++ b/src/app/features/landing/components/navigation/navigation.component.scss @@ -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; } + } -} \ No newline at end of file + &__cta { + display: flex; + justify-content: center; + + app-button { + width: 100%; + + ::ng-deep .nav-btn { + width: 100%; + justify-content: center; + } + } + } +} diff --git a/src/app/features/landing/components/navigation/navigation.component.ts b/src/app/features/landing/components/navigation/navigation.component.ts index 9f7d4ca..e09bb9e 100644 --- a/src/app/features/landing/components/navigation/navigation.component.ts +++ b/src/app/features/landing/components/navigation/navigation.component.ts @@ -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); + } + } } diff --git a/src/app/features/landing/components/projects/projects.component.scss b/src/app/features/landing/components/projects/projects.component.scss index 5baacb2..b5c9aba 100644 --- a/src/app/features/landing/components/projects/projects.component.scss +++ b/src/app/features/landing/components/projects/projects.component.scss @@ -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%; diff --git a/src/app/features/landing/pages/landingpage.component.html b/src/app/features/landing/pages/landingpage.component.html index cd174e0..d1a2dfa 100644 --- a/src/app/features/landing/pages/landingpage.component.html +++ b/src/app/features/landing/pages/landingpage.component.html @@ -3,4 +3,5 @@ + diff --git a/src/app/features/landing/pages/landingpage.component.ts b/src/app/features/landing/pages/landingpage.component.ts index 61400c1..8fff1ce 100644 --- a/src/app/features/landing/pages/landingpage.component.ts +++ b/src/app/features/landing/pages/landingpage.component.ts @@ -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', diff --git a/src/app/shared/ui/button/button.component.html b/src/app/shared/ui/button/button.component.html index a7ad9d8..c965377 100644 --- a/src/app/shared/ui/button/button.component.html +++ b/src/app/shared/ui/button/button.component.html @@ -1,16 +1,30 @@ @if (isRouteType) { - - @if (item.icon) { - - } - {{ item.label }} - + @if (disabled) { + + } @else { + + @if (item.icon) { + + } + {{ item.label }} + + } } diff --git a/src/app/shared/ui/button/button.component.scss b/src/app/shared/ui/button/button.component.scss index b97efe4..3f65c63 100644 --- a/src/app/shared/ui/button/button.component.scss +++ b/src/app/shared/ui/button/button.component.scss @@ -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; + } } \ No newline at end of file diff --git a/src/environments/n8n.ts b/src/environments/n8n.ts new file mode 100644 index 0000000..cb669b3 --- /dev/null +++ b/src/environments/n8n.ts @@ -0,0 +1,3 @@ +export const n8nEnvironment = { + contactWebhookUrl: 'https://n8n.hurler-webdesign.de/webhook/fbcced3d-503d-4bca-9147-405fe809253e', +};