openpanel integration und entwurf blog

This commit is contained in:
2026-04-03 17:05:16 +02:00
parent 5138005397
commit cd694d0776
45 changed files with 2558 additions and 310 deletions

View File

@@ -1,15 +1,30 @@
<section class="features-section" id="features-section">
<div class="features-section__wrapper">
<div class="features-section__grid centered">
@for (feature of featuresList; track feature.id) {
<div class="features-section__card">
<h3 class="features-section__claim">{{ feature.claim }}</h3>
<p class="features-section__description">{{ feature.description }}</p>
@if (feature.icon) {
<img [src]="feature.icon" [alt]="feature.iconDescription" />
}
</div>
}
</div>
<div class="features-section__wrapper">
<div class="features-section__header">
<h2>Warum Hurler Webdesign?</h2>
<p class="text-muted">Handwerk statt Baukasten das sind die Unterschiede, die zählen.</p>
</div>
</section>
<div class="features-section__grid">
@for (feature of featuresList; track feature.id; let i = $index) {
<div
#cardRef
class="features-section__card"
[style.--delay]="(i * 120) + 'ms'"
opTrack="feature_card_click"
[opTrackProps]="{ feature_id: feature.id, claim: feature.claim }">
<span class="features-section__card-number">0{{ feature.id }}</span>
<div class="features-section__icon-wrap">
<ng-icon [name]="feature.icon" class="features-section__icon"></ng-icon>
</div>
<h3 class="features-section__claim">{{ feature.claim }}</h3>
<p class="features-section__description">{{ feature.description }}</p>
<div class="features-section__bar"></div>
</div>
}
</div>
</div>
</section>

View File

@@ -1,55 +1,175 @@
@use 'abstracts';
@keyframes card-fade-up {
from {
opacity: 0;
transform: translateY(36px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.features-section {
min-height: calc(100vh - var(--neg-nav-height));
margin-top: var(--neg-nav-height);
min-height: 100vh;
display: flex;
align-items: center;
padding-block: calc(var(--space-4) * 2);
&__wrapper {
@include abstracts.container-wrapper;
width: 100%;
}
// ── 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);
}
}
// ── Grid ──────────────────────────────────────────────────────────────────
&__grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-3);
@include abstracts.breakpoint('md') {
grid-template-columns: repeat(3, 1fr);
.features-section__card {
&:nth-child(1) { grid-column: 1 / span 2; grid-row: 1; }
&:nth-child(2) { grid-column: 3; grid-row: 1; }
&:nth-child(3) { grid-column: 1; grid-row: 2; }
&:nth-child(4) { grid-column: 2 / span 2; grid-row: 2; }
}
}
}
// ── Card ──────────────────────────────────────────────────────────────────
&__card {
position: relative;
overflow: hidden;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
min-height: abstracts.rem(220);
display: flex;
flex-direction: column;
padding: var(--space-4) var(--space-3) var(--space-3);
gap: var(--space-2);
background-color: var(--bg-surface);
cursor: default;
// Scroll-entrance: start hidden
opacity: 0;
&.is-visible {
animation: card-fade-up 0.55s cubic-bezier(0.22, 1, 0.36, 1) var(--delay, 0ms) both;
}
// Hover: lift + deepen shadow
transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.25s ease,
border-color 0.25s ease;
&:hover {
transform: translateY(-5px);
border-color: var(--accent);
box-shadow:
0 8px 24px oklch(0% 0 0 / 0.07),
0 2px 6px oklch(0% 0 0 / 0.04);
}
@include abstracts.breakpoint('md') {
min-height: abstracts.rem(280);
}
}
// ── Ghost number (decorative) ─────────────────────────────────────────────
&__card-number {
position: absolute;
top: -0.1em;
right: var(--space-3);
font-size: 5rem;
font-weight: 900;
line-height: 1;
color: var(--accent);
opacity: 0.06;
pointer-events: none;
user-select: none;
transition: opacity 0.25s ease;
.features-section__card:hover & {
opacity: 0.1;
}
}
// ── Icon ─────────────────────────────────────────────────────────────────
&__icon-wrap {
display: flex;
align-items: center;
justify-content: center;
width: abstracts.rem(44);
height: abstracts.rem(44);
border-radius: 10px;
background-color: oklch(from var(--accent) l c h / 0.12);
flex-shrink: 0;
transition: background-color 0.25s ease, transform 0.25s cubic-bezier(0.22, 1, 0.36, 1);
&__wrapper {
@include abstracts.container-wrapper;
.features-section__card:hover & {
background-color: oklch(from var(--accent) l c h / 0.2);
transform: scale(1.1) rotate(-4deg);
}
}
&__grid {
width: 100%;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-3);
&__icon {
font-size: abstracts.rem(22);
color: var(--accent);
}
// ── Text ──────────────────────────────────────────────────────────────────
&__claim {
font-size: var(--font-size-lg);
font-weight: 700;
line-height: 1.2;
}
&__description {
font-size: var(--font-size-base);
color: var(--text-muted);
line-height: 1.65;
flex: 1;
}
// ── Accent bar (grows on hover) ───────────────────────────────────────────
&__bar {
position: absolute;
bottom: 0;
left: 0;
height: 3px;
width: 0;
background: linear-gradient(90deg, var(--accent), var(--accent-hover));
border-radius: 0 3px 0 var(--border-radius);
transition: width 0.4s cubic-bezier(0.22, 1, 0.36, 1);
.features-section__card:hover & {
width: 100%;
}
&__card {
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
height: abstracts.rem(300);
display: flex;
flex-direction: column;
padding-inline: var(--space-3);
justify-content: center;
&:nth-child(1) {
grid-column: 1 / span 2;
grid-row: 1;
}
&:nth-child(2) {
grid-column: 3;
grid-row: 1;
}
&:nth-child(3) {
grid-column: 1;
grid-row: 2;
}
&:nth-child(4) {
grid-column: 2 / span 2;
grid-row: 2;
}
}
&__claim {
font-size: var(--font-size-xl);
}
&__description {
font-size: var(--font-size-lg);
}
}
}
}

View File

@@ -1,40 +1,81 @@
import { Component } from '@angular/core';
import {
AfterViewInit,
Component,
ElementRef,
PLATFORM_ID,
QueryList,
ViewChildren,
inject,
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { cssCode, cssLock, cssDatabase, cssBrowser } from '@ng-icons/css.gg';
import { OpenPanelTrackDirective } from '@core/directives/openpanel.directive';
interface Features {
id: number,
claim: string,
description: string,
icon?: string,
iconDescription?: string
interface Feature {
id: number;
claim: string;
description: string;
icon: string;
}
@Component({
selector: 'app-features-section',
imports: [],
imports: [NgIcon, OpenPanelTrackDirective],
viewProviders: [provideIcons({ cssCode, cssLock, cssDatabase, cssBrowser })],
templateUrl: './features-section.component.html',
styleUrl: './features-section.component.scss',
})
export class FeaturesSectionComponent {
featuresList: Features[] = [
export class FeaturesSectionComponent implements AfterViewInit {
@ViewChildren('cardRef') cardElements!: QueryList<ElementRef<HTMLElement>>;
private readonly platformId = inject(PLATFORM_ID);
featuresList: Feature[] = [
{
id: 1,
claim: "Code statt Baukasten",
description: "Handgefertigte Performance, die Google und Ihre Nutzer lieben werden."
claim: 'Code statt Baukasten',
description:
'Handgefertigter Code statt träger WordPress-Templates. Ihre Seite lädt in unter einer Sekunde und das merken Google und Ihre Besucher.',
icon: 'cssCode',
},
{
id: 2,
claim: "Sicher per Design",
description: "Maximale Rechtskonformität durch eRecht24 und hauseigene Server-Infrastruktur."
claim: 'Sicher per Design',
description:
'Kein Plugin-Dschungel, keine veralteten CMS-Versionen. Maximale Rechtskonformität durch eRecht24-Integration und eine klar strukturierte Infrastruktur.',
icon: 'cssLock',
},
{
id: 3,
claim: "Heimat für Ihre Daten",
description: "Hosting und Services strikt nach europäischem Datenschutzstandard."
claim: 'Heimat für Ihre Daten',
description:
'Hosting und alle Services laufen ausschließlich auf europäischen Servern vollständig DSGVO-konform und ohne US-Cloudabhängigkeit.',
icon: 'cssDatabase',
},
{
id: 4,
claim: "Alles im Blick",
description: "Ein Portal für alles: Kommunikation, Verwaltung und Erfolgskontrolle"
claim: 'Alles im Blick',
description:
'Ein Verwaltungsportal für alles: Inhalte pflegen, Anfragen verwalten und Ihren Webauftritt jederzeit selbst aktualisieren ohne Programmierkenntnisse.',
icon: 'cssBrowser',
},
]
];
ngAfterViewInit(): void {
if (!isPlatformBrowser(this.platformId)) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1 }
);
this.cardElements.forEach((el) => observer.observe(el.nativeElement));
}
}

View File

@@ -1,8 +1,44 @@
<footer class="footer">
<div class="footer__wrapper">
Hurler Webdesign <br/>
Impressum <br/>
Über uns
<footer class="footer" id="contact">
<div class="footer__wrapper">
<div class="footer__grid">
<div class="footer__col footer__col--brand">
<div class="footer__logo">
<span class="footer__logo-icon">H</span>
<span><span class="footer__logo-accent">Hurler</span> Webdesign</span>
</div>
<p class="footer__tagline">
Handgefertigte Webseiten für Unternehmen und Vereine im Raum Nördlingen.
</p>
<address class="footer__address">
Untermagerbein 30<br />
86751 Mönchsdeggingen<br />
Bayern
</address>
</div>
<div class="footer__col">
<h4 class="footer__col-title">Navigation</h4>
<ul class="footer__links">
<li><a href="#hero" opTrack="footer_nav_click" [opTrackProps]="{ target: 'hero' }">Start</a></li>
<li><a href="#features-section" opTrack="footer_nav_click" [opTrackProps]="{ target: 'features' }">Vorteile</a></li>
<li><a href="#projects" opTrack="footer_nav_click" [opTrackProps]="{ target: 'projects' }">Projekte</a></li>
<li><a href="#pricing" opTrack="footer_nav_click" [opTrackProps]="{ target: 'pricing' }">Preise</a></li>
</ul>
</div>
<div class="footer__col">
<h4 class="footer__col-title">Rechtliches</h4>
<ul class="footer__links">
<li><a routerLink="/impressum" opTrack="footer_legal_click" [opTrackProps]="{ target: 'impressum' }">Impressum</a></li>
<li><a routerLink="/datenschutz" opTrack="footer_legal_click" [opTrackProps]="{ target: 'datenschutz' }">Datenschutz</a></li>
</ul>
</div>
</div>
</footer>
<div class="footer__bottom">
<p>&copy; {{ currentYear }} Hurler Webdesign. Alle Rechte vorbehalten.</p>
</div>
</div>
</footer>

View File

@@ -1,11 +1,102 @@
@use 'abstracts';
.footer {
background-color: var(--accent);
color: var(--bg-surface);
padding: 20px 0;
background-color: var(--accent);
color: var(--text-on-accent);
padding-top: var(--space-4);
&__wrapper {
@include abstracts.container-wrapper;
&__wrapper {
@include abstracts.container-wrapper;
}
&__grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-4);
padding-bottom: var(--space-4);
@include abstracts.breakpoint('md') {
grid-template-columns: 2fr 1fr 1fr;
}
}
}
&__col-title {
font-size: var(--font-size-base);
font-weight: 700;
margin-bottom: var(--space-3);
opacity: 0.7;
text-transform: uppercase;
letter-spacing: 0.05em;
}
&__logo {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--font-size-lg);
font-weight: 700;
margin-bottom: var(--space-3);
}
&__logo-icon {
display: flex;
align-items: center;
justify-content: center;
width: abstracts.rem(32);
height: abstracts.rem(32);
background-color: oklch(100% 0 0 / 0.2);
border: 1px solid oklch(100% 0 0 / 0.3);
border-radius: 5px;
font-weight: 700;
}
&__logo-accent {
// "Hurler" in slightly brighter shade for contrast on accent bg
opacity: 0.85;
}
&__tagline {
font-size: var(--font-size-base);
line-height: 1.6;
opacity: 0.8;
margin-bottom: var(--space-3);
max-width: 34ch;
}
&__address {
font-size: var(--font-size-base);
font-style: normal;
line-height: 1.7;
opacity: 0.7;
}
&__links {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: var(--space-2);
a {
font-size: var(--font-size-base);
opacity: 0.8;
transition: opacity 0.15s ease;
&:hover {
opacity: 1;
}
}
}
&__bottom {
border-top: 1px solid oklch(100% 0 0 / 0.15);
padding-block: var(--space-3);
text-align: center;
p {
font-size: 0.85rem;
opacity: 0.6;
}
}
}

View File

@@ -1,11 +1,13 @@
import { Component } from '@angular/core';
import { RouterModule } from '@angular/router';
import { OpenPanelTrackDirective } from '@core/directives/openpanel.directive';
@Component({
selector: 'app-footer',
imports: [],
imports: [RouterModule, OpenPanelTrackDirective],
templateUrl: './footer.component.html',
styleUrl: './footer.component.scss',
})
export class FooterComponent {
readonly currentYear = new Date().getFullYear();
}

View File

@@ -1,23 +1,31 @@
<section class="hero-section" id="hero">
<div class="hero-section__video-container">
<video autoplay muted loop>
<video autoplay muted loop playsinline>
<source src="/video/white_mit_black_stripes.webm" type="video/webm">
</video>
</div>
<div class="hero-section__wrapper">
<h1 class="hero-section__header">
Digitales Handwerk <br />
Digitales Handwerk<br />
statt Standard-Baukasten
</h1>
<p class="hero-section__claim">
Wir programmieren Blitzschnelle, sichere und maßgeschneiderte Webseiten für kleine,
mittelständische Unternehmen und Vereine. Ohne CMS-Ballast, dafür mit maximaler Performance.
Wir programmieren blitzschnelle, sichere und maßgeschneiderte Webseiten
für kleine Unternehmen und Vereine ohne CMS-Ballast, dafür mit maximaler Performance.
</p>
<div class="hero-section__links">
<app-button [item]="{ label: 'Über uns', type: 'anchor', target: '#about' }" variant="primary"></app-button>
<app-button (click)="onFeaturesClick()" [item]="{ label: 'Warum kein Wordpress', type: 'anchor', target: 'about'}"
variant="primary"></app-button>
<app-button
opTrack="hero_cta_features"
[opTrackProps]="{ location: 'hero' }"
[item]="{ label: 'Unsere Vorteile', type: 'anchor', target: '#features-section' }"
variant="primary">
</app-button>
<app-button
opTrack="hero_cta_pricing"
[opTrackProps]="{ location: 'hero' }"
[item]="{ label: 'Preise & Pakete', type: 'anchor', target: '#pricing' }"
variant="outline">
</app-button>
</div>
</div>
</section>
</section>

View File

@@ -1,7 +1,7 @@
@use "abstracts";
.hero-section {
position: relative; // WICHTIG: Bezugspunkt für das Video
position: relative;
min-height: calc(100vh + var(--nav-height));
margin-top: var(--neg-nav-height);
overflow: hidden;
@@ -21,6 +21,10 @@
&__wrapper {
@include abstracts.container-wrapper;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
&__video-container {
@@ -29,19 +33,20 @@
left: 0;
width: 100%;
height: 100%;
z-index: -2; // Hinter den Text legen
z-index: -2;
}
h1 {
color: var(--text-main); // Dein Wunsch-Style
&__header {
color: var(--text-main);
font-size: var(--font-size-xxl);
position: relative; // Stellt sicher, dass der Text über dem Video-Layer bleibt
position: relative;
margin-bottom: var(--space-4);
}
&__claim {
color: var(--text-main);
font-size: var(--font-size-xl);
font-size: var(--font-size-lg);
max-width: 60ch;
margin-bottom: var(--space-4);
}
@@ -50,18 +55,15 @@
flex-direction: row;
gap: var(--space-2);
justify-content: center;
flex-wrap: wrap;
}
video {
/* Das hier ist der entscheidende Teil */
width: 100%;
height: 100%;
object-fit: cover; // WICHTIG: Füllt den Container komplett aus, ohne zu verzerren
object-position: center; // Zentriert das Video, falls Ränder abgeschnitten werden
mask-image: linear-gradient(to bottom,
black 0%,
black 70%,
transparent 100%);
object-fit: cover;
object-position: center;
mask-image: linear-gradient(to bottom, black 0%, black 70%, transparent 100%);
-webkit-mask-image: linear-gradient(to bottom, black 0%, black 70%, transparent 100%);
}
}
}

View File

@@ -1,17 +1,11 @@
import { Component } from '@angular/core';
import { ButtonComponent } from '@shared/ui/button/button.component';
import { UmamiService } from '@core/services/umami.service';
import { OpenPanelTrackDirective } from '@core/directives/openpanel.directive';
@Component({
selector: 'app-hero',
imports: [ButtonComponent],
imports: [ButtonComponent, OpenPanelTrackDirective],
templateUrl: './hero.component.html',
styleUrl: './hero.component.scss',
})
export class HeroComponent {
constructor(private umami: UmamiService) {}
onFeaturesClick(): void {
this.umami.trackEvent('features-anchor-click')
}
}
export class HeroComponent {}

View File

@@ -2,7 +2,7 @@
<section class="header">
<div class="logo-container">
<span class="logo-container__logo centered">H</span>
<p class="logo-container__company" (click)="onFeaturesClick('lustiges Zeug')"><span>Hurler</span> Webdesign</p>
<p class="logo-container__company"><span>Hurler</span> Webdesign</p>
</div>
<div class="header__nav-section centered">
<div class="theme-toggle-container">
@@ -14,5 +14,4 @@
</div>
</div>
</section>
</div>
</div>

View File

@@ -29,6 +29,7 @@
.logo-container {
display: flex;
align-items: center;
justify-items: center;
gap: var(--space-2);
padding-left: var(--space-4);
@@ -48,6 +49,7 @@
font-size: var(--font-size-base);
font-weight: 700;
color: var(--text-main);
margin: auto;
span {
color: var(--accent);

View File

@@ -3,20 +3,13 @@ import { NgIcon } from '@ng-icons/core';
import { ToogleThemeComponent } from '@shared/utils/toogle-theme/toogle-theme.component';
import { NavMenuComponent } from '@shared/ui/nav-menu/nav-menu.component';
import { NavigationService } from '@core/services/navigation.service';
import { OpenPanelService } from '@core/services/openpanel.service';
import { OpenPanelTrackDirective } from '@core/directives/openpanel.directive';
@Component({
selector: 'app-navigation',
imports: [NgIcon, ToogleThemeComponent, NavMenuComponent, OpenPanelTrackDirective],
imports: [NgIcon, ToogleThemeComponent, NavMenuComponent],
templateUrl: './navigation.component.html',
styleUrl: './navigation.component.scss',
})
export class NavigationComponent {
protected readonly navigationService = inject(NavigationService);
private op = inject(OpenPanelService)
onFeaturesClick(blindplan: string): void {
this.op.track('features_clicked', { blindplan })
}
}

View File

@@ -1 +1,39 @@
<p>pricing works!</p>
<section class="pricing" id="pricing">
<div class="pricing__wrapper">
<div class="pricing__header">
<h2>Preise & Pakete</h2>
<p class="text-muted">Transparente Preise kein Abo, kein Versteckspiel.</p>
</div>
<div class="pricing__grid">
@for (tier of tiers; track tier.id) {
<div class="pricing__card" [class.pricing__card--highlighted]="tier.highlighted">
@if (tier.highlighted) {
<span class="pricing__badge">Beliebteste Wahl</span>
}
<div class="pricing__card-header">
<h3 class="pricing__tier-name">{{ tier.name }}</h3>
<div class="pricing__price">{{ tier.price }}</div>
<p class="pricing__price-note">{{ tier.priceNote }}</p>
</div>
<p class="pricing__description">{{ tier.description }}</p>
<ul class="pricing__features">
@for (feature of tier.features; track $index) {
<li class="pricing__feature-item">
<span class="pricing__check" aria-hidden="true"></span>
{{ feature }}
</li>
}
</ul>
<div class="pricing__cta">
<app-button
[opTrack]="'pricing_cta_click'"
[opTrackProps]="{ tier: tier.id, cta: tier.cta }"
[item]="{ label: tier.cta, type: 'anchor', target: '#contact' }"
[variant]="tier.highlighted ? 'primary' : 'outline'">
</app-button>
</div>
</div>
}
</div>
</div>
</section>

View File

@@ -0,0 +1,142 @@
@use 'abstracts';
.pricing {
min-height: 100vh;
display: flex;
align-items: center;
padding-block: var(--space-4);
background-color: var(--bg-muted);
&__wrapper {
@include abstracts.container-wrapper;
width: 100%;
}
&__header {
text-align: center;
margin-bottom: var(--space-4);
h2 {
font-size: var(--font-size-xl);
margin-bottom: var(--space-2);
}
p {
font-size: var(--font-size-lg);
}
}
&__grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-3);
align-items: start;
@include abstracts.breakpoint('md') {
grid-template-columns: repeat(3, 1fr);
align-items: stretch;
}
}
&__card {
position: relative;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: var(--space-4) var(--space-3);
display: flex;
flex-direction: column;
gap: var(--space-3);
background-color: var(--bg-surface);
transition: box-shadow 0.2s ease;
&:hover {
box-shadow: 0 8px 32px oklch(0% 0 0 / 0.08);
}
&--highlighted {
border-color: var(--accent);
box-shadow: 0 4px 24px oklch(45% 0.22 250 / 0.15);
&:hover {
box-shadow: 0 8px 32px oklch(45% 0.22 250 / 0.2);
}
}
}
&__badge {
position: absolute;
top: -1px;
left: 50%;
transform: translateX(-50%) translateY(-50%);
background-color: var(--accent);
color: var(--text-on-accent);
font-size: 0.75rem;
font-weight: 600;
padding: 4px 12px;
border-radius: 999px;
white-space: nowrap;
}
&__card-header {
text-align: center;
}
&__tier-name {
font-size: var(--font-size-lg);
font-weight: 700;
margin-bottom: var(--space-2);
}
&__price {
font-size: var(--font-size-xl);
font-weight: 700;
color: var(--accent);
margin-bottom: var(--space-1);
}
&__price-note {
font-size: 0.8rem;
color: var(--text-muted);
}
&__description {
font-size: var(--font-size-base);
color: var(--text-muted);
line-height: 1.6;
text-align: center;
}
&__features {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: var(--space-2);
flex: 1;
}
&__feature-item {
display: flex;
align-items: flex-start;
gap: var(--space-2);
font-size: var(--font-size-base);
line-height: 1.5;
}
&__check {
color: var(--accent);
font-weight: 700;
flex-shrink: 0;
}
&__cta {
display: flex;
justify-content: center;
margin-top: auto;
app-button {
width: 100%;
}
}
}

View File

@@ -1,11 +1,76 @@
import { Component } from '@angular/core';
import { OpenPanelTrackDirective } from '@core/directives/openpanel.directive';
import { ButtonComponent } from '@shared/ui/button/button.component';
interface PricingTier {
id: string;
name: string;
price: string;
priceNote: string;
description: string;
features: string[];
cta: string;
highlighted: boolean;
}
@Component({
selector: 'app-pricing',
imports: [],
imports: [OpenPanelTrackDirective, ButtonComponent],
templateUrl: './pricing.component.html',
styleUrl: './pricing.component.scss',
})
export class PricingComponent {
tiers: PricingTier[] = [
{
id: 'starter',
name: 'Starter',
price: '799 €',
priceNote: 'einmalig zzgl. MwSt.',
description: 'Ideal für Handwerker und Vereine mit klarem Fokus auf eine starke Online-Präsenz.',
features: [
'1-Pager / Landingpage',
'Individuelles Design',
'Suchmaschinenoptimierung (SEO)',
'Kontaktformular',
'Cookie-Banner & Datenschutz',
'12 Monate Hosting inklusive',
],
cta: 'Jetzt anfragen',
highlighted: false,
},
{
id: 'business',
name: 'Business',
price: '1.499 €',
priceNote: 'einmalig zzgl. MwSt.',
description: 'Für Unternehmen, die mehr wollen: mehrere Seiten, eigenes CMS-Portal und Analysen.',
features: [
'Mehrseiter (bis 5 Seiten)',
'Alles aus Starter',
'Verwaltungsportal (CMS)',
'Blog / Neuigkeiten',
'Performance-Analyse',
'Prioritäts-Support',
],
cta: 'Jetzt anfragen',
highlighted: true,
},
{
id: 'individual',
name: 'Individual',
price: 'Auf Anfrage',
priceNote: 'individuelles Angebot',
description: 'Shops, Web-Applikationen, API-Anbindungen wir setzen komplexe Projekte um.',
features: [
'Online-Shops',
'Web-Applikationen',
'API-Integration',
'Individuelle Funktionen',
'Langfristige Betreuung',
'Auf Ihre Bedürfnisse zugeschnitten',
],
cta: 'Kontakt aufnehmen',
highlighted: false,
},
];
}

View File

@@ -1,20 +1,27 @@
<section class="projects" id="projects">
<div class="projects__wrapper">
<div class="projects__card-container centered">
@for(project of projects; track project.id) {
<div class="projects__card">
<img [src]="project.image" />
<div class="projects__card__description">
<h3>{{ project.company }}</h3>
<p>{{ project.shortDescription }}</p>
<div class="projects__card-features">
@for(feature of project.features; track $index) {
<p>{{ feature }}</p>
}
</div>
</div>
</div>
}
</div>
<div class="projects__wrapper">
<div class="projects__header">
<h2>Unsere Projekte</h2>
<p class="text-muted">Echte Webseiten für echte Unternehmen sehen Sie selbst.</p>
</div>
</section>
<div class="projects__card-container">
@for (project of projects; track project.id) {
<div
class="projects__card"
opTrack="project_card_click"
[opTrackProps]="{ project_id: project.id, company: project.company }">
<img [src]="project.image" [alt]="project.company" />
<div class="projects__card__overlay">
<h3>{{ project.company }}</h3>
<p>{{ project.shortDescription }}</p>
<div class="projects__card-features">
@for (feature of project.features; track $index) {
<span class="projects__tag">{{ feature }}</span>
}
</div>
</div>
</div>
}
</div>
</div>
</section>

View File

@@ -1,45 +1,99 @@
@use 'abstracts';
.projects {
min-height: 100vh;
display: flex;
align-items: center;
padding-block: var(--space-4);
&__wrapper {
@include abstracts.container-wrapper;
width: 100%;
}
&__header {
text-align: center;
margin-bottom: var(--space-4);
h2 {
font-size: var(--font-size-xl);
margin-bottom: var(--space-2);
}
p {
font-size: var(--font-size-lg);
}
}
&__card-container {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-3);
@include abstracts.breakpoint('md') {
grid-template-columns: repeat(3, 1fr);
}
}
&__card {
position: relative;
border-radius: var(--border-radius);
overflow: hidden;
aspect-ratio: 4 / 3;
background-color: var(--bg-muted);
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.4s ease;
}
&:hover img {
transform: scale(1.04);
}
&__overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: var(--space-3);
background: linear-gradient(to top, oklch(0% 0 0 / 0.8) 0%, transparent 60%);
opacity: 0;
transition: opacity 0.3s ease-in-out;
color: var(--color-white);
h3 {
font-size: var(--font-size-lg);
margin-bottom: var(--space-1);
}
p {
font-size: var(--font-size-base);
margin-bottom: var(--space-2);
opacity: 0.85;
}
}
&:hover &__overlay {
opacity: 1;
}
}
&__card-features {
display: flex;
min-height: 100vh;
margin-top: var(--neg-nav-height);
align-items: center;
flex-wrap: wrap;
gap: var(--space-1);
}
&__wrapper {
@include abstracts.container-wrapper;
}
&__card-container {
display: flex;
gap: var(--space-3);
}
&__card {
position: relative;
border-radius: var(--border-radius);
overflow: hidden;
&__description {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: var(--space-2);
background: rgba(0, 0, 0, 0.7);
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
&:hover &__description {
opacity: 1;
color: var(--color-white);
}
}
&__card-features {
display: flex;
gap: var(--space-2);
}
}
&__tag {
font-size: 0.75rem;
padding: 2px 8px;
border-radius: 999px;
border: 1px solid oklch(100% 0 0 / 0.4);
color: var(--color-white);
}
}

View File

@@ -1,16 +1,17 @@
import { Component } from '@angular/core';
import { OpenPanelTrackDirective } from '@core/directives/openpanel.directive';
interface Project {
id: number,
image: string,
company: string,
shortDescription: string,
features: string[]
id: number;
image: string;
company: string;
shortDescription: string;
features: string[];
}
@Component({
selector: 'app-projects',
imports: [],
imports: [OpenPanelTrackDirective],
templateUrl: './projects.component.html',
styleUrl: './projects.component.scss',
})
@@ -18,24 +19,24 @@ export class ProjectsComponent {
projects: Project[] = [
{
id: 1,
company: "Backerei Müller",
image: "/images/bakery.jpg",
shortDescription: "Landingpage mit wechselnden Angeboten",
features: ["SEO", "Angebote", "Dark/Light"],
company: 'Schreiner Müller GmbH',
image: '/images/schreiner-mueller.jpg',
shortDescription: 'Handwerkswebsite mit Leistungsübersicht und Kontaktformular',
features: ['SEO', 'Kontaktformular', 'Dark/Light'],
},
{
id: 2,
company: "Backerei Müller",
image: "/images/bakery.jpg",
shortDescription: "Landingpage mit wechselnden Angeboten",
features: ["SEO", "Angebote", "Dark/Light"],
company: 'Schützenverein Nördlingen e.V.',
image: '/images/schuetzenverein.jpg',
shortDescription: 'Vereinswebsite mit Terminen und Veranstaltungskalender',
features: ['SEO', 'Termine', 'Mitglieder'],
},
{
id: 3,
company: "Backerei Müller",
image: "/images/bakery.jpg",
shortDescription: "Landingpage mit wechselnden Angeboten",
features: ["SEO", "Angebote", "Dark/Light"],
}
]
company: 'Bäckerei Huber',
image: '/images/baeckerei-huber.jpg',
shortDescription: 'Landingpage mit täglich wechselnden Tagesangeboten',
features: ['SEO', 'Angebote', 'Responsive'],
},
];
}

View File

@@ -2,4 +2,5 @@
<app-hero></app-hero>
<app-features-section></app-features-section>
<app-projects></app-projects>
<app-footer></app-footer>
<app-pricing></app-pricing>
<app-footer></app-footer>

View File

@@ -4,6 +4,7 @@ import { HeroComponent } from '../components/hero/hero.component';
import { FeaturesSectionComponent } from '../components/features-section/features-section.component';
import { FooterComponent } from '../components/footer/footer.component';
import { ProjectsComponent } from '../components/projects/projects.component';
import { PricingComponent } from '../components/pricing/pricing.component';
import { SeoService } from '@core/services/seo.service';
@Component({
@@ -13,8 +14,9 @@ import { SeoService } from '@core/services/seo.service';
HeroComponent,
FeaturesSectionComponent,
ProjectsComponent,
PricingComponent,
FooterComponent,
],
],
templateUrl: './landingpage.component.html',
styleUrl: './landingpage.component.scss',
})