openpanel integration und entwurf blog
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>© {{ currentYear }} Hurler Webdesign. Alle Rechte vorbehalten.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user