Landingpage design geändert. routes ergänzt für projects, project detail ergänzt

This commit is contained in:
2026-04-12 15:37:32 +02:00
parent 11e2553549
commit 012636ec35
28 changed files with 1880 additions and 266 deletions

View File

@@ -15,5 +15,10 @@ export const routes: Routes = [
path: 'blog/:slug',
loadComponent: () => import('@features/blog').then(m => m.BlogDetailComponent),
data: { trackName: 'BlogDetail' }
},
{
path: 'projekt/:slug',
loadComponent: () => import('@features/landing/pages/project-detail/project-detail.component').then(m => m.ProjectDetailComponent),
data: { trackName: 'ProjectDetail' }
}
];

View File

@@ -24,6 +24,7 @@
@if (getCoverUrl(post); as coverUrl) {
<div class="blog-detail__cover">
<img [src]="coverUrl" [alt]="post.title" />
<div class="blog-detail__cover-overlay"></div>
</div>
}
@@ -40,17 +41,46 @@
<h1 class="blog-detail__title">{{ post.title }}</h1>
<time class="blog-detail__date" [dateTime]="post.published_at">
{{ post.published_at | date:'d. MMMM yyyy':'':'de' }}
</time>
<div class="blog-detail__meta">
<time class="blog-detail__date" [dateTime]="post.published_at">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
<line x1="16" y1="2" x2="16" y2="6"></line>
<line x1="8" y1="2" x2="8" y2="6"></line>
<line x1="3" y1="10" x2="21" y2="10"></line>
</svg>
{{ post.published_at | date:'d. MMMM yyyy':'':'de' }}
</time>
<span class="blog-detail__read-time">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
5 Min. Lesezeit
</span>
</div>
<p class="blog-detail__summary">{{ post.summary }}</p>
</header>
<hr class="blog-detail__divider" />
<article class="blog-detail__content" [innerHTML]="parsedContent()"></article>
<footer class="blog-detail__footer">
<div class="blog-detail__share">
<span class="blog-detail__share-label">Artikel teilen:</span>
<a href="#" class="blog-detail__share-btn" aria-label="Auf Twitter teilen">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</a>
<a href="#" class="blog-detail__share-btn" aria-label="Auf LinkedIn teilen">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
</a>
</div>
</footer>
</div>
}

View File

@@ -19,95 +19,111 @@
max-width: 740px;
}
// ── Cover ──────────────────────────────────────────────────────────────
&__cover {
position: relative;
display: flex;
width: 100%;
height: abstracts.em(500);
height: clamp(300px, 50vh, 500px);
overflow: hidden;
margin-bottom: calc(var(--space-4) * 1.5);
img {
margin: auto;
width: auto;
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
// ── Header ─────────────────────────────────────────────────────────────
&__cover-overlay {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 50%;
background: linear-gradient(to top, var(--bg-surface) 0%, transparent 100%);
pointer-events: none;
}
&__header {
display: flex;
flex-direction: column;
gap: var(--space-2);
margin-bottom: var(--space-4);
&:not(:first-child) {
margin-top: calc(var(--space-4) * 1.5);
}
gap: var(--space-3);
margin-bottom: calc(var(--space-4) * 1.5);
}
&__tags {
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
gap: var(--space-2);
}
&__tag {
font-size: 0.75rem;
font-weight: 600;
padding: 3px 10px;
font-size: 0.7rem;
font-weight: 700;
padding: 4px 12px;
border-radius: 999px;
background-color: oklch(from var(--accent) l c h / 0.12);
background: linear-gradient(135deg, oklch(from var(--accent) l c h / 0.12) 0%, oklch(from var(--accent) l c h / 0.08) 100%);
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.05em;
}
&__title {
font-size: var(--font-size-xl);
font-weight: 700;
line-height: 1.2;
font-size: clamp(var(--font-size-xl), 4vw, 3rem);
font-weight: 800;
line-height: 1.15;
letter-spacing: -0.02em;
}
&__date {
font-size: 0.85rem;
&__meta {
display: flex;
align-items: center;
gap: var(--space-4);
flex-wrap: wrap;
}
&__date,
&__read-time {
display: flex;
align-items: center;
gap: var(--space-1);
font-size: 0.875rem;
color: var(--text-muted);
svg {
opacity: 0.7;
}
}
&__summary {
font-size: var(--font-size-lg);
color: var(--text-muted);
line-height: 1.6;
line-height: 1.65;
font-weight: 400;
}
&__divider {
border: none;
padding-top: var(--space-3);
border-top: 1px solid var(--border-color);
margin-block: var(--space-4);
}
// ── Article content (from Directus HTML) ──────────────────────────────
&__content {
font-size: var(--font-size-base);
line-height: 1.75;
line-height: 1.8;
color: var(--text-main);
h2 {
font-size: var(--font-size-xl);
font-weight: 700;
margin-top: calc(var(--space-4) * 1.5);
margin-bottom: var(--space-3);
line-height: 1.25;
}
h3 {
font-size: var(--font-size-lg);
font-weight: 700;
margin-top: var(--space-4);
margin-bottom: var(--space-2);
}
h3 {
font-size: var(--font-size-base);
font-weight: 700;
margin-top: var(--space-3);
margin-bottom: var(--space-2);
line-height: 1.3;
}
p {
@@ -118,6 +134,7 @@
color: var(--accent);
text-decoration: underline;
text-underline-offset: 3px;
transition: color 0.2s ease;
&:hover {
color: var(--accent-hover);
@@ -130,34 +147,35 @@
margin-bottom: var(--space-3);
li {
margin-bottom: var(--space-1);
margin-bottom: var(--space-2);
}
}
blockquote {
border-left: 3px solid var(--accent);
padding-left: var(--space-3);
padding-left: var(--space-4);
margin-inline: 0;
margin-block: var(--space-3);
margin-block: var(--space-4);
color: var(--text-muted);
font-style: italic;
font-size: var(--font-size-lg);
}
code {
font-family: 'Courier New', Courier, monospace;
font-size: 0.9em;
background-color: var(--bg-muted);
padding: 2px 6px;
border-radius: 4px;
padding: 3px 8px;
border-radius: 6px;
}
pre {
background-color: var(--bg-muted);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: var(--space-3);
padding: var(--space-4);
overflow-x: auto;
margin-bottom: var(--space-3);
margin-bottom: var(--space-4);
code {
background: none;
@@ -169,7 +187,8 @@
img {
max-width: 100%;
border-radius: var(--border-radius);
margin-block: var(--space-3);
margin-block: var(--space-4);
box-shadow: 0 4px 20px oklch(0% 0 0 / 0.08);
}
hr {
@@ -179,7 +198,41 @@
}
}
// ── Not Found ──────────────────────────────────────────────────────────
&__footer {
margin-top: calc(var(--space-4) * 2);
padding-top: var(--space-4);
border-top: 1px solid var(--border-color);
}
&__share {
display: flex;
align-items: center;
gap: var(--space-2);
}
&__share-label {
font-size: 0.875rem;
font-weight: 600;
color: var(--text-muted);
}
&__share-btn {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
background-color: var(--bg-muted);
color: var(--text-main);
transition: background-color 0.2s ease, color 0.2s ease, transform 0.2s ease;
&:hover {
background-color: var(--accent);
color: var(--text-on-accent);
transform: scale(1.1);
}
}
&__not-found {
padding-top: calc(var(--space-4) * 2);
@@ -191,11 +244,9 @@
}
}
// ── Skeleton ───────────────────────────────────────────────────────────
&__cover-skeleton {
width: 100%;
height: 380px;
height: clamp(300px, 50vh, 500px);
background-color: var(--bg-muted);
margin-bottom: calc(var(--space-4) * 1.5);
}

View File

@@ -4,8 +4,9 @@
<div class="blog-list__wrapper">
<header class="blog-list__header">
<span class="blog-list__label">Wissen & Insights</span>
<h1>Blog</h1>
<p class="text-muted">Einblicke, Tipps und Hintergründe rund um Webdesign &amp; digitale Präsenz.</p>
<p class="text-muted">Tipps, Hintergründe und Best Practices rund um Webdesign &amp; digitale Präsenz.</p>
</header>
@if (loading()) {
@@ -42,8 +43,16 @@
@if (getCoverUrl(post); as coverUrl) {
<img [src]="coverUrl" [alt]="post.title" loading="lazy" />
} @else {
<div class="blog-card__image-placeholder"></div>
<div class="blog-card__image-placeholder">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M19 20H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h10l6 6v8a2 2 0 0 1-2 2Z"/>
<path d="m14 14-3 3-2-2-4 4"/>
</svg>
</div>
}
<div class="blog-card__read-time">
<span>5 Min. Lesezeit</span>
</div>
</div>
<div class="blog-card__body">
@@ -56,9 +65,17 @@
}
<h2 class="blog-card__title">{{ post.title }}</h2>
<p class="blog-card__summary">{{ post.summary }}</p>
<time class="blog-card__date" [dateTime]="post.published_at">
{{ post.published_at | date:'d. MMMM yyyy':'':'de' }}
</time>
<div class="blog-card__footer">
<time class="blog-card__date" [dateTime]="post.published_at">
{{ post.published_at | date:'d. MMM yyyy':'':'de' }}
</time>
<span class="blog-card__cta">
Weiterlesen
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</span>
</div>
</div>
</a>

View File

@@ -27,6 +27,16 @@
}
}
&__label {
display: inline-block;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--accent);
margin-bottom: var(--space-2);
}
&__grid {
display: grid;
grid-template-columns: 1fr;
@@ -52,8 +62,6 @@
}
}
// ── Blog Card ────────────────────────────────────────────────────────────────
.blog-card {
display: flex;
flex-direction: column;
@@ -62,17 +70,18 @@
overflow: hidden;
background-color: var(--bg-surface);
color: var(--text-main);
transition: transform 0.22s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.22s ease,
border-color 0.22s ease;
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(-4px);
transform: translateY(-6px);
border-color: var(--accent);
box-shadow: 0 8px 24px oklch(0% 0 0 / 0.07);
box-shadow: 0 16px 48px oklch(0% 0 0 / 0.1);
}
&__image {
position: relative;
aspect-ratio: 16 / 9;
overflow: hidden;
background-color: var(--bg-muted);
@@ -86,7 +95,7 @@
transition: transform 0.4s ease;
.blog-card:hover & {
transform: scale(1.04);
transform: scale(1.05);
}
}
}
@@ -94,7 +103,25 @@
&__image-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--bg-muted) 0%, var(--border-color) 100%);
color: var(--text-muted);
opacity: 0.5;
}
&__read-time {
position: absolute;
bottom: var(--space-2);
right: var(--space-2);
background: oklch(0% 0 0 / 0.7);
backdrop-filter: blur(8px);
color: var(--color-white);
font-size: 0.7rem;
font-weight: 600;
padding: 4px 10px;
border-radius: 999px;
}
&__body {
@@ -112,39 +139,65 @@
}
&__tag {
font-size: 0.75rem;
font-weight: 600;
padding: 2px 8px;
font-size: 0.7rem;
font-weight: 700;
padding: 3px 10px;
border-radius: 999px;
background-color: oklch(from var(--accent) l c h / 0.12);
background: linear-gradient(135deg, oklch(from var(--accent) l c h / 0.12) 0%, oklch(from var(--accent) l c h / 0.08) 100%);
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.05em;
}
&__title {
font-size: var(--font-size-lg);
font-weight: 700;
line-height: 1.25;
line-height: 1.3;
transition: color 0.2s ease;
.blog-card:hover & {
color: var(--accent);
}
}
&__summary {
font-size: var(--font-size-base);
color: var(--text-muted);
line-height: 1.6;
line-height: 1.65;
flex: 1;
// Clamp to 3 lines
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
&__date {
font-size: 0.85rem;
color: var(--text-muted);
&__footer {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: var(--space-2);
border-top: 1px solid var(--border-color);
margin-top: auto;
}
// ── Skeleton ────────────────────────────────────────────────────────────
&__date {
font-size: 0.8rem;
color: var(--text-muted);
}
&__cta {
display: flex;
align-items: center;
gap: var(--space-1);
font-size: 0.8rem;
font-weight: 600;
color: var(--accent);
transition: gap 0.2s ease;
.blog-card:hover & {
gap: var(--space-2);
}
}
&--skeleton {
pointer-events: none;

View File

@@ -1,8 +1,9 @@
<section class="features-section" id="features-section">
<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>
<span class="features-section__label">Unsere Vorteile</span>
<h2>Warum Kunden uns wählen</h2>
<p class="text-muted">Kein Baukasten, keine Kompromisse nur echtes Handwerk für Ihre digitale Präsenz.</p>
</div>
<div class="features-section__grid">
@for (feature of featuresList; track feature.id; let i = $index) {
@@ -13,8 +14,6 @@
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>
@@ -22,7 +21,10 @@
<h3 class="features-section__claim">{{ feature.claim }}</h3>
<p class="features-section__description">{{ feature.description }}</p>
<div class="features-section__bar"></div>
<div class="features-section__benefit">
<span class="features-section__check"></span>
{{ feature.benefit }}
</div>
</div>
}
</div>

View File

@@ -3,7 +3,7 @@
@keyframes card-fade-up {
from {
opacity: 0;
transform: translateY(36px);
transform: translateY(32px);
}
to {
opacity: 1;
@@ -22,8 +22,6 @@
width: 100%;
}
// ── Header ────────────────────────────────────────────────────────────────
&__header {
text-align: center;
margin-bottom: calc(var(--space-4) * 1.5);
@@ -38,7 +36,15 @@
}
}
// ── Grid ──────────────────────────────────────────────────────────────────
&__label {
display: inline-block;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--accent);
margin-bottom: var(--space-2);
}
&__grid {
display: grid;
@@ -46,130 +52,98 @@
gap: var(--space-3);
@include abstracts.breakpoint('md') {
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(2, 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; }
}
@include abstracts.breakpoint('lg') {
grid-template-columns: repeat(4, 1fr);
}
}
// ── 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);
padding: var(--space-4);
gap: var(--space-3);
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);
transform: translateY(-4px);
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);
box-shadow: 0 12px 32px oklch(0% 0 0 / 0.08);
}
}
// ── 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);
width: abstracts.rem(48);
height: abstracts.rem(48);
border-radius: 12px;
background: linear-gradient(135deg, oklch(from var(--accent) l c h / 0.15) 0%, oklch(from var(--accent) l c h / 0.08) 100%);
flex-shrink: 0;
transition: background-color 0.25s ease, transform 0.25s cubic-bezier(0.22, 1, 0.36, 1);
transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
.features-section__card:hover & {
background-color: oklch(from var(--accent) l c h / 0.2);
transform: scale(1.1) rotate(-4deg);
transform: scale(1.08);
}
}
&__icon {
font-size: abstracts.rem(22);
font-size: abstracts.rem(24);
color: var(--accent);
}
// ── Text ──────────────────────────────────────────────────────────────────
&__claim {
font-size: var(--font-size-lg);
font-weight: 700;
line-height: 1.2;
line-height: 1.3;
color: var(--text-main);
}
&__description {
font-size: var(--font-size-base);
color: var(--text-muted);
line-height: 1.65;
line-height: 1.6;
flex: 1;
}
// ── Accent bar (grows on hover) ───────────────────────────────────────────
&__benefit {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: 0.85rem;
font-weight: 600;
color: var(--accent);
padding-top: var(--space-2);
border-top: 1px solid var(--border-color);
margin-top: auto;
}
&__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%;
}
&__check {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: oklch(from var(--accent) l c h / 0.15);
font-size: 0.7rem;
flex-shrink: 0;
}
}

View File

@@ -16,6 +16,7 @@ interface Feature {
id: number;
claim: string;
description: string;
benefit: string;
icon: string;
}
@@ -33,30 +34,34 @@ export class FeaturesSectionComponent implements AfterViewInit {
featuresList: Feature[] = [
{
id: 1,
claim: 'Code statt Baukasten',
claim: 'Blitzschnelle Ladezeiten',
description:
'Handgefertigter Code statt träger WordPress-Templates. Ihre Seite lädt in unter einer Sekunde und das merken Google und Ihre Besucher.',
benefit: 'Besseres Google-Ranking & weniger Absprünge',
icon: 'cssCode',
},
{
id: 2,
claim: 'Sicher per Design',
claim: 'Maximale Sicherheit',
description:
'Kein Plugin-Dschungel, keine veralteten CMS-Versionen. Maximale Rechtskonformität durch eRecht24-Integration und eine klar strukturierte Infrastruktur.',
benefit: 'Kein Risiko durch Sicherheitslücken',
icon: 'cssLock',
},
{
id: 3,
claim: 'Heimat für Ihre Daten',
claim: 'Europäisches Hosting',
description:
'Hosting und alle Services laufen ausschließlich auf europäischen Servern vollständig DSGVO-konform und ohne US-Cloudabhängigkeit.',
benefit: '100% DSGVO-konform & datenschutzrechtlich sicher',
icon: 'cssDatabase',
},
{
id: 4,
claim: 'Alles im Blick',
claim: 'Einfaches Dashboard',
description:
'Ein Verwaltungsportal für alles: Inhalte pflegen, Anfragen verwalten und Ihren Webauftritt jederzeit selbst aktualisieren ohne Programmierkenntnisse.',
benefit: 'Zeitersparnis & Unabhängigkeit',
icon: 'cssBrowser',
},
];

View File

@@ -1,9 +1,9 @@
@use 'abstracts';
.footer {
background-color: var(--accent);
color: var(--text-on-accent);
padding-top: var(--space-4);
background-color: var(--text-main);
color: var(--bg-surface);
padding-top: calc(var(--space-4) * 2);
&__wrapper {
@include abstracts.container-wrapper;
@@ -12,8 +12,8 @@
&__grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-4);
padding-bottom: var(--space-4);
gap: calc(var(--space-4) * 1.5);
padding-bottom: calc(var(--space-4) * 1.5);
@include abstracts.breakpoint('md') {
grid-template-columns: 2fr 1fr 1fr;
@@ -21,12 +21,12 @@
}
&__col-title {
font-size: var(--font-size-base);
font-size: 0.75rem;
font-weight: 700;
margin-bottom: var(--space-3);
opacity: 0.7;
text-transform: uppercase;
letter-spacing: 0.05em;
letter-spacing: 0.1em;
color: var(--accent);
}
&__logo {
@@ -36,38 +36,38 @@
font-size: var(--font-size-lg);
font-weight: 700;
margin-bottom: var(--space-3);
color: var(--bg-surface);
}
&__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;
width: abstracts.rem(36);
height: abstracts.rem(36);
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%);
border-radius: 8px;
font-weight: 700;
color: var(--text-on-accent);
}
&__logo-accent {
// "Hurler" in slightly brighter shade for contrast on accent bg
opacity: 0.85;
color: var(--accent);
}
&__tagline {
font-size: var(--font-size-base);
line-height: 1.6;
opacity: 0.8;
line-height: 1.7;
margin-bottom: var(--space-3);
max-width: 34ch;
color: var(--bg-muted);
}
&__address {
font-size: var(--font-size-base);
font-style: normal;
line-height: 1.7;
opacity: 0.7;
color: var(--bg-muted);
}
&__links {
@@ -80,23 +80,24 @@
a {
font-size: var(--font-size-base);
opacity: 0.8;
transition: opacity 0.15s ease;
color: var(--bg-muted);
transition: color 0.2s ease;
&:hover {
opacity: 1;
color: var(--accent);
}
}
}
&__bottom {
border-top: 1px solid oklch(100% 0 0 / 0.15);
border-top: 1px solid var(--border-color);
padding-block: var(--space-3);
text-align: center;
color: var(--text-muted);
p {
font-size: 0.85rem;
opacity: 0.6;
color: var(--text-muted);
}
}
}

View File

@@ -5,9 +5,13 @@
</video>
</div>
<div class="hero-section__wrapper">
<div class="hero-section__badge">
<span class="hero-section__badge-dot"></span>
Jetzt neue Webseite sichern bis zu 3 Monate kostenloses Hosting
</div>
<h1 class="hero-section__header">
Digitales Handwerk<br />
statt Standard-Baukasten
Webseiten, die<br />
<span class="hero-section__header-accent">Kunden überzeugen</span>
</h1>
<p class="hero-section__claim">
Wir programmieren blitzschnelle, sichere und maßgeschneiderte Webseiten
@@ -17,15 +21,26 @@
<app-button
opTrack="hero_cta_features"
[opTrackProps]="{ location: 'hero' }"
[item]="{ label: 'Unsere Vorteile', type: 'anchor', target: '#features-section' }"
[item]="{ label: 'Vorteile entdecken', 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' }"
[item]="{ label: 'Preise ansehen', type: 'anchor', target: '#pricing' }"
variant="outline">
</app-button>
</div>
<div class="hero-section__social-proof">
<div class="hero-section__avatars">
<div class="hero-section__avatar">MK</div>
<div class="hero-section__avatar">AS</div>
<div class="hero-section__avatar">JW</div>
</div>
<div class="hero-section__proof-text">
<div class="hero-section__stars">★★★★★</div>
<span>Von 50+ Kunden empfohlen</span>
</div>
</div>
</div>
</section>

View File

@@ -25,6 +25,7 @@
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-4);
}
&__video-container {
@@ -36,18 +37,49 @@
z-index: -2;
}
&__badge {
display: inline-flex;
align-items: center;
gap: var(--space-2);
background: oklch(from var(--accent) l c h / 0.1);
border: 1px solid oklch(from var(--accent) l c h / 0.2);
padding: var(--space-1) var(--space-3);
border-radius: 999px;
font-size: 0.875rem;
font-weight: 500;
color: var(--accent);
animation: badge-pulse 2s ease-in-out infinite;
}
&__badge-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--accent);
animation: dot-pulse 1.5s ease-in-out infinite;
}
&__header {
color: var(--text-main);
font-size: var(--font-size-xxl);
font-weight: 800;
line-height: 1.1;
position: relative;
margin-bottom: var(--space-4);
max-width: 14ch;
}
&__header-accent {
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
&__claim {
color: var(--text-main);
font-size: var(--font-size-lg);
max-width: 60ch;
margin-bottom: var(--space-4);
max-width: 55ch;
line-height: 1.6;
}
&__links {
@@ -58,6 +90,61 @@
flex-wrap: wrap;
}
&__social-proof {
display: flex;
align-items: center;
gap: var(--space-3);
margin-top: var(--space-2);
padding-top: var(--space-3);
border-top: 1px solid var(--border-color);
width: 100%;
max-width: 400px;
justify-content: center;
}
&__avatars {
display: flex;
margin-right: calc(var(--space-2) * -1);
}
&__avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%);
border: 2px solid var(--bg-surface);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 700;
color: var(--text-on-accent);
margin-left: -10px;
&:first-child {
margin-left: 0;
}
}
&__proof-text {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
}
&__stars {
color: oklch(75% 0.18 45);
font-size: 0.875rem;
letter-spacing: 1px;
}
&__proof-text span {
font-size: 0.8rem;
color: var(--text-muted);
font-weight: 500;
}
video {
width: 100%;
height: 100%;
@@ -67,3 +154,13 @@
-webkit-mask-image: linear-gradient(to bottom, black 0%, black 70%, transparent 100%);
}
}
@keyframes badge-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.85; }
}
@keyframes dot-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.2); }
}

View File

@@ -1,8 +1,9 @@
<section class="pricing" id="pricing">
<div class="pricing__wrapper">
<div class="pricing__header">
<span class="pricing__label">Transparent & Fair</span>
<h2>Preise & Pakete</h2>
<p class="text-muted">Transparente Preise kein Abo, kein Versteckspiel.</p>
<p class="text-muted">Kein Abo-Modell. Keine versteckten Kosten. Sie besitzen Ihre Webseite.</p>
</div>
<div class="pricing__grid">
@for (tier of tiers; track tier.id) {
@@ -35,5 +36,19 @@
</div>
}
</div>
<div class="pricing__trust">
<div class="pricing__trust-item">
<span class="pricing__trust-icon">🔒</span>
<span>30 Tage Geld-zurück-Garantie</span>
</div>
<div class="pricing__trust-item">
<span class="pricing__trust-icon">🏆</span>
<span>Persönliche Betreuung inklusive</span>
</div>
<div class="pricing__trust-item">
<span class="pricing__trust-icon"></span>
<span>Innerhalb von 4 Wochen fertig</span>
</div>
</div>
</div>
</section>

View File

@@ -26,6 +26,16 @@
}
}
&__label {
display: inline-block;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--accent);
margin-bottom: var(--space-2);
}
&__grid {
display: grid;
grid-template-columns: 1fr;
@@ -42,23 +52,26 @@
position: relative;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: var(--space-4) var(--space-3);
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-3);
background-color: var(--bg-surface);
transition: box-shadow 0.2s ease;
transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.25s ease;
&:hover {
box-shadow: 0 8px 32px oklch(0% 0 0 / 0.08);
transform: translateY(-4px);
box-shadow: 0 12px 40px oklch(0% 0 0 / 0.1);
}
&--highlighted {
border-color: var(--accent);
box-shadow: 0 4px 24px oklch(45% 0.22 250 / 0.15);
background: linear-gradient(180deg, oklch(from var(--accent) l c h / 0.03) 0%, var(--bg-surface) 100%);
&:hover {
box-shadow: 0 8px 32px oklch(45% 0.22 250 / 0.2);
box-shadow: 0 12px 40px oklch(45% 0.22 250 / 0.2);
}
}
}
@@ -68,17 +81,20 @@
top: -1px;
left: 50%;
transform: translateX(-50%) translateY(-50%);
background-color: var(--accent);
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%);
color: var(--text-on-accent);
font-size: 0.75rem;
font-weight: 600;
padding: 4px 12px;
padding: 4px 14px;
border-radius: 999px;
white-space: nowrap;
box-shadow: 0 2px 8px oklch(45% 0.22 250 / 0.3);
}
&__card-header {
text-align: center;
padding-bottom: var(--space-3);
border-bottom: 1px solid var(--border-color);
}
&__tier-name {
@@ -89,7 +105,7 @@
&__price {
font-size: var(--font-size-xl);
font-weight: 700;
font-weight: 800;
color: var(--accent);
margin-bottom: var(--space-1);
}
@@ -139,4 +155,27 @@
width: 100%;
}
}
&__trust {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: var(--space-4);
margin-top: calc(var(--space-4) * 1.5);
padding-top: calc(var(--space-4) * 1.5);
border-top: 1px solid var(--border-color);
}
&__trust-item {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: 0.875rem;
font-weight: 500;
color: var(--text-muted);
}
&__trust-icon {
font-size: 1.25rem;
}
}

View File

@@ -1,26 +1,48 @@
<section class="projects" id="projects">
<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>
<span class="projects__label">Erfolgsgeschichten</span>
<h2>Projekte, die überzeugen</h2>
<p class="text-muted">So helfen wir Unternehmen, online erfolgreich zu sein.</p>
</div>
<div class="projects__card-container">
@for (project of projects; track project.id) {
<div
@for (project of projects; track project.id; let i = $index) {
<a
[routerLink]="['/projekt', project.slug]"
class="projects__card"
[style.--delay]="(i * 100) + 'ms'"
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>
[opTrackProps]="{ project_id: project.id, company: project.company, slug: project.slug }">
<div class="projects__card-image">
<img [src]="project.image" [alt]="project.company" loading="lazy" />
<div class="projects__card-overlay"></div>
</div>
<div class="projects__card-content">
<span class="projects__card-branch">{{ project.branch }}</span>
<h3 class="projects__card-title">{{ project.company }}</h3>
<p class="projects__card-description">{{ project.shortDescription }}</p>
<div class="projects__card-features">
@for (feature of project.features; track $index) {
<span class="projects__tag">{{ feature }}</span>
}
</div>
@if (project.result) {
<div class="projects__card-result">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline>
<polyline points="17 6 23 6 23 12"></polyline>
</svg>
<span>{{ project.result }}</span>
</div>
}
<span class="projects__card-cta">
Projekt ansehen
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</span>
</div>
</div>
</a>
}
</div>
</div>

View File

@@ -1,10 +1,19 @@
@use 'abstracts';
@keyframes card-fade-up {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.projects {
min-height: 100vh;
display: flex;
align-items: center;
padding-block: var(--space-4);
padding-block: calc(var(--space-4) * 2);
background-color: var(--bg-muted);
&__wrapper {
@include abstracts.container-wrapper;
@@ -13,7 +22,7 @@
&__header {
text-align: center;
margin-bottom: var(--space-4);
margin-bottom: calc(var(--space-4) * 1.5);
h2 {
font-size: var(--font-size-xl);
@@ -25,6 +34,16 @@
}
}
&__label {
display: inline-block;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--accent);
margin-bottom: var(--space-2);
}
&__card-container {
display: grid;
grid-template-columns: 1fr;
@@ -39,12 +58,28 @@
position: relative;
border-radius: var(--border-radius);
overflow: hidden;
aspect-ratio: 4 / 3;
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)
);
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
cursor: pointer;
text-decoration: none;
color: inherit;
display: flex;
flex-direction: column;
transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.3s ease;
animation: card-fade-up 0.55s cubic-bezier(0.22, 1, 0.36, 1) var(--delay, 0ms) both;
&:hover {
transform: translateY(-6px);
box-shadow: 0 20px 50px oklch(0% 0 0 / 0.12);
}
}
&__card-image {
position: relative;
aspect-ratio: 16 / 10;
overflow: hidden;
img {
width: 100%;
@@ -54,50 +89,94 @@
transition: transform 0.4s ease;
}
&:hover img {
transform: scale(1.04);
.projects__card:hover & img {
transform: scale(1.05);
}
}
&__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);
&__card-overlay {
position: absolute;
inset: 0;
background: linear-gradient(
to top,
oklch(0% 0 0 / 0.4) 0%,
transparent 50%
);
pointer-events: none;
}
h3 {
font-size: var(--font-size-lg);
margin-bottom: var(--space-1);
}
&__card-content {
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-2);
}
p {
font-size: var(--font-size-base);
margin-bottom: var(--space-2);
opacity: 0.85;
}
}
&__card-branch {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--accent);
}
&:hover &__overlay {
opacity: 1;
}
&__card-title {
font-size: var(--font-size-lg);
font-weight: 700;
line-height: 1.3;
color: var(--text-main);
}
&__card-description {
font-size: var(--font-size-base);
color: var(--text-muted);
line-height: 1.6;
}
&__card-features {
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
margin-top: var(--space-1);
}
&__tag {
font-size: 0.75rem;
padding: 2px 8px;
font-size: 0.7rem;
font-weight: 600;
padding: 4px 10px;
border-radius: 999px;
border: 1px solid oklch(100% 0 0 / 0.4);
color: var(--color-white);
background: linear-gradient(135deg, oklch(from var(--accent) l c h / 0.12) 0%, oklch(from var(--accent) l c h / 0.08) 100%);
color: var(--accent);
}
&__card-result {
display: flex;
align-items: center;
gap: var(--space-2);
margin-top: var(--space-2);
padding-top: var(--space-2);
border-top: 1px solid var(--border-color);
font-size: 0.875rem;
font-weight: 700;
color: var(--accent);
svg {
flex-shrink: 0;
}
}
&__card-cta {
display: flex;
align-items: center;
gap: var(--space-1);
font-size: 0.875rem;
font-weight: 600;
color: var(--accent);
margin-top: var(--space-2);
transition: gap 0.2s ease;
.projects__card:hover & {
gap: var(--space-2);
}
}
}

View File

@@ -1,17 +1,21 @@
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { OpenPanelTrackDirective } from '@core/directives/openpanel.directive';
interface Project {
id: number;
slug: string;
image: string;
company: string;
branch: string;
shortDescription: string;
features: string[];
result?: string;
}
@Component({
selector: 'app-projects',
imports: [OpenPanelTrackDirective],
imports: [RouterLink, OpenPanelTrackDirective],
templateUrl: './projects.component.html',
styleUrl: './projects.component.scss',
})
@@ -19,24 +23,33 @@ export class ProjectsComponent {
projects: Project[] = [
{
id: 1,
company: 'Schreiner Müller GmbH',
image: '/images/schreiner-mueller.jpg',
shortDescription: 'Handwerkswebsite mit Leistungsübersicht und Kontaktformular',
features: ['SEO', 'Kontaktformular', 'Dark/Light'],
slug: 'metzgerei-schlachthof-qualitaet',
company: 'Metzgerei Schlachthof-Qualität',
branch: 'Fleischerei & Metzgerei',
image: '/images/projekte/metzgerei.jpg',
shortDescription: 'Premium-Webauftritt für traditionelle Metzgerei mit Fleischerei in der Region',
features: ['SEO-Optimierung', 'Responsive Design', 'DSGVO-konform'],
result: '+40% mehr Anfragen über Website',
},
{
id: 2,
company: 'Schützenverein Nördlingen e.V.',
image: '/images/schuetzenverein.jpg',
shortDescription: 'Vereinswebsite mit Terminen und Veranstaltungskalender',
features: ['SEO', 'Termine', 'Mitglieder'],
slug: 'finanzberatung-vermoegenswert',
company: 'Finanzberatung Vermögenswert',
branch: 'Finanzdienstleistung',
image: '/images/projekte/finanzberatung.jpg',
shortDescription: 'Vertrauenswürdiger Online-Auftritt für unabhängige Finanzberatung',
features: ['Lead-Generierung', 'Terminbuchung', 'Premium-Design'],
result: '+60% neue Terminbuchungen',
},
{
id: 3,
company: 'Bäckerei Huber',
image: '/images/baeckerei-huber.jpg',
shortDescription: 'Landingpage mit täglich wechselnden Tagesangeboten',
features: ['SEO', 'Angebote', 'Responsive'],
slug: 'physiotherapie-beweglich',
company: 'Physiotherapie Beweglich',
branch: 'Gesundheitswesen',
image: '/images/projekte/physiotherapie.jpg',
shortDescription: 'Moderne Praxis-Website für Physiotherapie und Rehabilitation',
features: ['Online-Terminbuchung', 'Leistungen', 'Google-optimiert'],
result: 'Top 3 bei Google-Suche',
},
];
}

View File

@@ -0,0 +1,19 @@
<section class="stats" id="stats">
<div class="stats__wrapper">
<div class="stats__header">
<h2>Zahlen, die für sich sprechen</h2>
<p class="text-muted">Transparent. Messbar. Vertrauenswürdig.</p>
</div>
<div class="stats__grid">
@for (stat of stats; track stat.id; let i = $index) {
<div class="stats__card" #statRef>
<div class="stats__value">
{{ displayedValues()[i] }}{{ stat.suffix }}
</div>
<div class="stats__label">{{ stat.label }}</div>
<div class="stats__description">{{ stat.description }}</div>
</div>
}
</div>
</div>
</section>

View File

@@ -0,0 +1,90 @@
@use 'abstracts';
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.stats {
padding-block: calc(var(--space-4) * 2);
background: linear-gradient(180deg, var(--bg-surface) 0%, var(--bg-muted) 100%);
&__wrapper {
@include abstracts.container-wrapper;
width: 100%;
}
&__header {
text-align: center;
margin-bottom: calc(var(--space-4) * 1.5);
h2 {
font-size: var(--font-size-xl);
margin-bottom: var(--space-2);
}
}
&__grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-3);
@include abstracts.breakpoint('md') {
grid-template-columns: repeat(4, 1fr);
}
}
&__card {
text-align: center;
padding: var(--space-4) var(--space-3);
border-radius: var(--border-radius);
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
opacity: 0;
&.is-visible {
animation: fade-in-up 0.6s cubic-bezier(0.22, 1, 0.36, 1) both;
}
&:nth-child(1).is-visible { animation-delay: 0ms; }
&:nth-child(2).is-visible { animation-delay: 100ms; }
&:nth-child(3).is-visible { animation-delay: 200ms; }
&:nth-child(4).is-visible { animation-delay: 300ms; }
&:hover {
border-color: var(--accent);
transform: translateY(-2px);
transition: transform 0.2s ease, border-color 0.2s ease;
}
}
&__value {
font-size: clamp(2.5rem, 5vw + 1rem, 4rem);
font-weight: 800;
line-height: 1;
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: var(--space-2);
}
&__label {
font-size: var(--font-size-base);
font-weight: 700;
color: var(--text-main);
margin-bottom: var(--space-1);
}
&__description {
font-size: 0.8rem;
color: var(--text-muted);
line-height: 1.4;
}
}

View File

@@ -0,0 +1,95 @@
import { Component, AfterViewInit, ElementRef, ViewChildren, QueryList, inject, PLATFORM_ID, signal } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
interface Stat {
id: number;
value: number;
suffix: string;
label: string;
description: string;
}
@Component({
selector: 'app-stats',
templateUrl: './stats.component.html',
styleUrl: './stats.component.scss',
})
export class StatsComponent implements AfterViewInit {
@ViewChildren('statRef') statElements!: QueryList<ElementRef<HTMLElement>>;
private platformId = inject(PLATFORM_ID);
displayedValues = signal<number[]>([0, 0, 0, 0]);
stats: Stat[] = [
{
id: 1,
value: 50,
suffix: '+',
label: 'Projekte',
description: 'Webseiten erfolgreich umgesetzt',
},
{
id: 2,
value: 99,
suffix: '%',
label: 'Kundenzufriedenheit',
description: 'Würden uns weiterempfehlen',
},
{
id: 3,
value: 7,
suffix: '+',
label: 'Jahre Erfahrung',
description: 'Im Webdesign & Development',
},
{
id: 4,
value: 100,
suffix: '%',
label: 'DSGVO-konform',
description: 'European Hosting & Datenschutz',
},
];
ngAfterViewInit(): void {
if (!isPlatformBrowser(this.platformId)) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
this.animateValues();
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.3 }
);
this.statElements.forEach((el) => observer.observe(el.nativeElement));
}
private animateValues(): void {
const duration = 2000;
const steps = 60;
const stepDuration = duration / steps;
this.stats.forEach((stat, index) => {
let current = 0;
const increment = stat.value / steps;
const timer = setInterval(() => {
current += increment;
if (current >= stat.value) {
current = stat.value;
clearInterval(timer);
}
this.displayedValues.update((values) => {
const newValues = [...values];
newValues[index] = Math.floor(current);
return newValues;
});
}, stepDuration);
});
}
}

View File

@@ -0,0 +1,46 @@
<section class="testimonials" id="testimonials">
<div class="testimonials__wrapper">
<div class="testimonials__header">
<h2>Das sagen unsere Kunden</h2>
<p class="text-muted">Echte Ergebnisse für echte Unternehmen.</p>
</div>
<div class="testimonials__grid">
@for (testimonial of testimonials; track testimonial.id; let i = $index) {
<div
class="testimonials__card"
[style.--delay]="(i * 150) + 'ms'"
opTrack="testimonial_view"
[opTrackProps]="{ testimonial_id: testimonial.id, company: testimonial.company }">
<div class="testimonials__quote-icon">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none">
<path d="M10 8C6.686 8 4 10.686 4 14v10c0 1.1.9 2 2 2h2c1.1 0 2-.9 2-2v-6c0-1.1.9-2 2-2h4c1.1 0 2 .9 2 2v6c0 1.1.9 2 2 2h2c1.1 0 2-.9 2-2V14c0-3.314-2.686-6-6-6h-6z" fill="currentColor" opacity="0.15"/>
</svg>
</div>
<div class="testimonials__stars">
@for (star of [1,2,3,4,5]; track star) {
<span [class.filled]="star <= testimonial.rating"></span>
}
</div>
<blockquote class="testimonials__text">
"{{ testimonial.quote }}"
</blockquote>
<div class="testimonials__author">
<div
class="testimonials__avatar"
[style.background]="'linear-gradient(135deg, ' + testimonial.gradientFrom + ', ' + testimonial.gradientTo + ')'">
{{ testimonial.initials }}
</div>
<div class="testimonials__author-info">
<div class="testimonials__name">{{ testimonial.name }}</div>
<div class="testimonials__role">{{ testimonial.role }}, {{ testimonial.company }}</div>
</div>
</div>
</div>
}
</div>
</div>
</section>

View File

@@ -0,0 +1,128 @@
@use 'abstracts';
@keyframes card-fade-up {
from {
opacity: 0;
transform: translateY(32px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.testimonials {
padding-block: calc(var(--space-4) * 2);
background-color: var(--bg-muted);
&__wrapper {
@include abstracts.container-wrapper;
width: 100%;
}
&__header {
text-align: center;
margin-bottom: calc(var(--space-4) * 1.5);
h2 {
font-size: var(--font-size-xl);
margin-bottom: var(--space-2);
}
}
&__grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-3);
@include abstracts.breakpoint('md') {
grid-template-columns: repeat(3, 1fr);
}
}
&__card {
position: relative;
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-3);
transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.25s ease;
animation: card-fade-up 0.55s cubic-bezier(0.22, 1, 0.36, 1) var(--delay, 0ms) both;
&:hover {
transform: translateY(-4px);
box-shadow: 0 12px 32px oklch(0% 0 0 / 0.08);
}
}
&__quote-icon {
color: var(--accent);
opacity: 0.6;
position: absolute;
top: var(--space-3);
right: var(--space-3);
}
&__stars {
display: flex;
gap: 2px;
font-size: 1rem;
color: var(--border-color);
.filled {
color: oklch(75% 0.18 45);
}
}
&__text {
font-size: var(--font-size-base);
line-height: 1.7;
color: var(--text-main);
font-style: normal;
flex: 1;
margin: 0;
}
&__author {
display: flex;
align-items: center;
gap: var(--space-2);
padding-top: var(--space-2);
border-top: 1px solid var(--border-color);
}
&__avatar {
width: 44px;
height: 44px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
font-weight: 700;
color: var(--text-on-accent);
flex-shrink: 0;
}
&__author-info {
display: flex;
flex-direction: column;
gap: 2px;
}
&__name {
font-weight: 700;
font-size: var(--font-size-base);
color: var(--text-main);
}
&__role {
font-size: 0.8rem;
color: var(--text-muted);
}
}

View File

@@ -0,0 +1,61 @@
import { Component } from '@angular/core';
import { OpenPanelTrackDirective } from '@core/directives/openpanel.directive';
interface Testimonial {
id: number;
name: string;
role: string;
company: string;
quote: string;
rating: number;
initials: string;
gradientFrom: string;
gradientTo: string;
}
@Component({
selector: 'app-testimonials',
imports: [OpenPanelTrackDirective],
templateUrl: './testimonials.component.html',
styleUrl: './testimonials.component.scss',
})
export class TestimonialsComponent {
testimonials: Testimonial[] = [
{
id: 1,
name: 'Markus Krause',
role: 'Geschäftsführer',
company: 'Krause Metallbau GmbH',
quote:
'Endlich eine Webseite, die nicht aussieht wie jede andere Handwerker-Homepage. Seit dem Relaunch haben wir 40% mehr Anfragen über die Website.',
rating: 5,
initials: 'MK',
gradientFrom: 'oklch(45% 0.22 250)',
gradientTo: 'oklch(55% 0.22 250)',
},
{
id: 2,
name: 'Anna Schlüter',
role: 'Vorstandsvorsitzende',
company: 'Turnverein Blau-Weiß 09',
quote:
'Der neue Online-Auftritt hat uns geholfen, jüngere Mitglieder anzusprechen. Das Verwaltungsportal spart uns enorm viel Zeit.',
rating: 5,
initials: 'AS',
gradientFrom: 'oklch(70% 0.15 250)',
gradientTo: 'oklch(65% 0.18 250)',
},
{
id: 3,
name: 'Jan Winkler',
role: 'Inhaber',
company: 'Winkler IT-Services',
quote:
'Professionell, zuverlässig und super Kommunikation. Die Seite lädt rasend schnell und unser Google-Ranking hat sich deutlich verbessert.',
rating: 5,
initials: 'JW',
gradientFrom: 'oklch(55% 0.2 250)',
gradientTo: 'oklch(60% 0.22 250)',
},
];
}

View File

@@ -1,7 +1,9 @@
<app-navigation></app-navigation>
<app-hero></app-hero>
<app-stats></app-stats>
<app-features-section></app-features-section>
<app-projects></app-projects>
<app-testimonials></app-testimonials>
<app-pricing></app-pricing>
<app-contact></app-contact>
<app-footer></app-footer>

View File

@@ -1,11 +1,13 @@
import { Component, OnInit, inject } from '@angular/core';
import { NavigationComponent } from '../components/navigation/navigation.component';
import { HeroComponent } from '../components/hero/hero.component';
import { StatsComponent } from '../components/stats/stats.component';
import { FeaturesSectionComponent } from '../components/features-section/features-section.component';
import { FooterComponent } from '../components/footer/footer.component';
import { TestimonialsComponent } from '../components/testimonials/testimonials.component';
import { ProjectsComponent } from '../components/projects/projects.component';
import { PricingComponent } from '../components/pricing/pricing.component';
import { ContactComponent } from '../components/contact/contact.component';
import { FooterComponent } from '../components/footer/footer.component';
import { SeoService } from '@core/services/seo.service';
@Component({
@@ -13,7 +15,9 @@ import { SeoService } from '@core/services/seo.service';
imports: [
NavigationComponent,
HeroComponent,
StatsComponent,
FeaturesSectionComponent,
TestimonialsComponent,
ProjectsComponent,
PricingComponent,
ContactComponent,

View File

@@ -0,0 +1,187 @@
<main class="project-detail">
@if (project(); as p) {
<section class="project-detail__hero">
<div class="project-detail__hero-image">
<img [src]="p.image" [alt]="p.company" />
<div class="project-detail__hero-overlay"></div>
</div>
<div class="project-detail__hero-content">
<a routerLink="/" class="project-detail__back">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 12H5M12 19l-7-7 7-7"/>
</svg>
Zurück
</a>
<span class="project-detail__branch">{{ p.branch }}</span>
<h1 class="project-detail__title">{{ p.company }}</h1>
<p class="project-detail__description">{{ p.description }}</p>
@if (p.result) {
<div class="project-detail__result">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline>
<polyline points="17 6 23 6 23 12"></polyline>
</svg>
<span>{{ p.result }}</span>
</div>
}
</div>
</section>
<section class="project-detail__features">
<div class="project-detail__wrapper">
<h2 class="project-detail__section-title">Leistungen im Projekt</h2>
<div class="project-detail__features-grid">
@for (feature of p.features; track feature.title; let i = $index) {
<div class="project-detail__feature" [style.--delay]="(i * 100) + 'ms'">
<div class="project-detail__feature-icon">
@switch (feature.icon) {
@case ('search') {
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
}
@case ('smartphone') {
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="5" y="2" width="14" height="20" rx="2" ry="2"></rect>
<line x1="12" y1="18" x2="12.01" y2="18"></line>
</svg>
}
@case ('shield') {
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
</svg>
}
@case ('target') {
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<circle cx="12" cy="12" r="6"></circle>
<circle cx="12" cy="12" r="2"></circle>
</svg>
}
@case ('calendar') {
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
<line x1="16" y1="2" x2="16" y2="6"></line>
<line x1="8" y1="2" x2="8" y2="6"></line>
<line x1="3" y1="10" x2="21" y2="10"></line>
</svg>
}
@case ('award') {
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="8" r="7"></circle>
<polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline>
</svg>
}
@case ('calendar-check') {
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
<line x1="16" y1="2" x2="16" y2="6"></line>
<line x1="8" y1="2" x2="8" y2="6"></line>
<line x1="3" y1="10" x2="21" y2="10"></line>
<path d="m9 16 2 2 4-4"></path>
</svg>
}
@case ('list') {
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="8" y1="6" x2="21" y2="6"></line>
<line x1="8" y1="12" x2="21" y2="12"></line>
<line x1="8" y1="18" x2="21" y2="18"></line>
<line x1="3" y1="6" x2="3.01" y2="6"></line>
<line x1="3" y1="12" x2="3.01" y2="12"></line>
<line x1="3" y1="18" x2="3.01" y2="18"></line>
</svg>
}
@case ('google') {
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<path d="M12 8v8"></path>
<path d="M8 12h8"></path>
</svg>
}
}
</div>
<div class="project-detail__feature-content">
<h3>{{ feature.title }}</h3>
<p>{{ feature.description }}</p>
</div>
</div>
}
</div>
</div>
</section>
@if (p.testimonial) {
<section class="project-detail__testimonial">
<div class="project-detail__wrapper project-detail__wrapper--narrow">
<blockquote class="project-detail__quote">
<svg class="project-detail__quote-icon" width="48" height="48" viewBox="0 0 24 24" fill="currentColor">
<path d="M10 8c-3.314 0-6 2.686-6 6v10c0 1.1.9 2 2 2h2c1.1 0 2-.9 2-2v-6c0-1.1.9-2 2-2h4c1.1 0 2 .9 2 2v6c0 1.1.9 2 2 2h2c1.1 0 2-.9 2-2V14c0-3.314-2.686-6-6-6h-6z"/>
</svg>
<p>"{{ p.testimonial.quote }}"</p>
<footer>
<cite>
<strong>{{ p.testimonial.author }}</strong>
<span>{{ p.testimonial.role }}, {{ p.company }}</span>
</cite>
</footer>
</blockquote>
</div>
</section>
}
<section class="project-detail__cta">
<div class="project-detail__wrapper">
<h2>Erfolgreich online wie Ihr Projekt</h2>
<p>Lassen Sie uns gemeinsam Ihr nächstes Projekt realisieren.</p>
<a routerLink="/#contact" class="project-detail__cta-button">
Projekt anfragen
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</a>
</div>
</section>
@if (otherProjects().length > 0) {
<section class="project-detail__more">
<div class="project-detail__wrapper">
<h2 class="project-detail__section-title">Weitere Projekte</h2>
<div class="project-detail__more-grid">
@for (other of otherProjects(); track other.slug) {
<a
[routerLink]="['/projekt', other.slug]"
class="project-detail__more-card">
<div class="project-detail__more-image">
<img [src]="other.image" [alt]="other.company" loading="lazy" />
</div>
<div class="project-detail__more-content">
<span class="project-detail__more-branch">{{ other.branch }}</span>
<h3>{{ other.company }}</h3>
<span class="project-detail__more-cta">
Projekt ansehen
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</span>
</div>
</a>
}
</div>
</div>
</section>
}
} @else {
<div class="project-detail__not-found">
<div class="project-detail__wrapper">
<h1>Projekt nicht gefunden</h1>
<p>Das gesuchte Projekt existiert leider nicht.</p>
<a routerLink="/" class="project-detail__back-link">Zurück zur Startseite</a>
</div>
</div>
}
</main>

View File

@@ -0,0 +1,391 @@
@use 'abstracts';
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.project-detail {
min-height: calc(100vh - var(--nav-height));
&__wrapper {
@include abstracts.container-wrapper;
width: 100%;
&--narrow {
max-width: 740px;
}
}
// ── Hero ─────────────────────────────────────────────────────────────────────
&__hero {
position: relative;
}
&__hero-image {
position: relative;
width: 100%;
height: clamp(400px, 60vh, 600px);
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
&__hero-overlay {
position: absolute;
inset: 0;
background: linear-gradient(
to top,
oklch(from var(--bg-surface) l c h) 0%,
oklch(from var(--bg-surface) l c h / 0.8) 30%,
oklch(from var(--bg-surface) l c h / 0.4) 60%,
transparent 100%
);
}
&__hero-content {
position: relative;
margin-top: -200px;
padding-bottom: calc(var(--space-4) * 2);
@include abstracts.container-wrapper;
max-width: 800px;
animation: fade-in-up 0.6s ease-out both;
}
&__back {
display: inline-flex;
align-items: center;
gap: var(--space-2);
font-size: 0.875rem;
font-weight: 500;
color: var(--text-muted);
margin-bottom: var(--space-4);
transition: color 0.2s ease;
&:hover {
color: var(--accent);
}
}
&__branch {
display: inline-block;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--accent);
margin-bottom: var(--space-2);
}
&__title {
font-size: clamp(var(--font-size-xl), 5vw, 3rem);
font-weight: 800;
line-height: 1.1;
letter-spacing: -0.02em;
margin-bottom: var(--space-3);
}
&__description {
font-size: var(--font-size-lg);
color: var(--text-muted);
line-height: 1.7;
margin-bottom: var(--space-4);
}
&__result {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
background: linear-gradient(135deg, oklch(from var(--accent) l c h / 0.15) 0%, oklch(from var(--accent) l c h / 0.08) 100%);
border: 1px solid oklch(from var(--accent) l c h / 0.2);
border-radius: 999px;
font-size: 0.875rem;
font-weight: 700;
color: var(--accent);
svg {
flex-shrink: 0;
}
}
// ── Features ─────────────────────────────────────────────────────────────────
&__features {
padding-block: calc(var(--space-4) * 2);
background-color: var(--bg-muted);
}
&__section-title {
font-size: var(--font-size-xl);
font-weight: 700;
margin-bottom: calc(var(--space-4) * 1.5);
text-align: center;
}
&__features-grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-3);
@include abstracts.breakpoint('md') {
grid-template-columns: repeat(3, 1fr);
}
}
&__feature {
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-3);
transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.25s ease;
animation: fade-in-up 0.55s cubic-bezier(0.22, 1, 0.36, 1) var(--delay, 0ms) both;
&:hover {
transform: translateY(-4px);
box-shadow: 0 12px 32px oklch(0% 0 0 / 0.08);
}
}
&__feature-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border-radius: 12px;
background: linear-gradient(135deg, oklch(from var(--accent) l c h / 0.15) 0%, oklch(from var(--accent) l c h / 0.08) 100%);
color: var(--accent);
flex-shrink: 0;
}
&__feature-content {
h3 {
font-size: var(--font-size-base);
font-weight: 700;
margin-bottom: var(--space-1);
}
p {
font-size: var(--font-size-base);
color: var(--text-muted);
line-height: 1.6;
}
}
// ── Testimonial ──────────────────────────────────────────────────────────────
&__testimonial {
padding-block: calc(var(--space-4) * 2);
background-color: var(--bg-surface);
}
&__quote {
position: relative;
text-align: center;
padding: var(--space-4);
}
&__quote-icon {
color: var(--accent);
opacity: 0.15;
margin-bottom: var(--space-3);
}
&__quote p {
font-size: var(--font-size-xl);
font-weight: 500;
line-height: 1.5;
color: var(--text-main);
margin-bottom: var(--space-4);
font-style: italic;
}
&__quote footer cite {
display: flex;
flex-direction: column;
gap: 4px;
font-style: normal;
strong {
font-weight: 700;
color: var(--text-main);
}
span {
font-size: 0.875rem;
color: var(--text-muted);
}
}
// ── CTA ─────────────────────────────────────────────────────────────────────
&__cta {
padding-block: calc(var(--space-4) * 2);
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%);
text-align: center;
h2 {
font-size: var(--font-size-xl);
font-weight: 700;
color: var(--text-on-accent);
margin-bottom: var(--space-2);
}
p {
font-size: var(--font-size-lg);
color: oklch(from var(--text-on-accent) l c h / 0.85);
margin-bottom: var(--space-4);
}
}
&__cta-button {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
background-color: var(--bg-surface);
color: var(--accent);
font-weight: 700;
font-size: var(--font-size-base);
border-radius: 999px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:hover {
transform: scale(1.05);
box-shadow: 0 8px 24px oklch(0% 0 0 / 0.2);
}
}
// ── More Projects ────────────────────────────────────────────────────────────
&__more {
padding-block: calc(var(--space-4) * 2);
}
&__more-grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-3);
@include abstracts.breakpoint('md') {
grid-template-columns: repeat(2, 1fr);
}
}
&__more-card {
display: flex;
flex-direction: column;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
overflow: hidden;
background-color: var(--bg-surface);
transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.25s ease;
&:hover {
transform: translateY(-4px);
box-shadow: 0 12px 32px oklch(0% 0 0 / 0.08);
}
}
&__more-image {
aspect-ratio: 16 / 9;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.4s ease;
.project-detail__more-card:hover & {
transform: scale(1.05);
}
}
}
&__more-content {
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-1);
h3 {
font-size: var(--font-size-lg);
font-weight: 700;
}
}
&__more-branch {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--accent);
}
&__more-cta {
display: flex;
align-items: center;
gap: var(--space-1);
font-size: 0.875rem;
font-weight: 600;
color: var(--accent);
margin-top: var(--space-2);
transition: gap 0.2s ease;
.project-detail__more-card:hover & {
gap: var(--space-2);
}
}
// ── Not Found ────────────────────────────────────────────────────────────────
&__not-found {
padding-block: calc(var(--space-4) * 3);
text-align: center;
h1 {
font-size: var(--font-size-xl);
margin-bottom: var(--space-2);
}
p {
color: var(--text-muted);
margin-bottom: var(--space-4);
}
}
&__back-link {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
background-color: var(--accent);
color: var(--text-on-accent);
font-weight: 600;
border-radius: 999px;
transition: background-color 0.2s ease;
&:hover {
background-color: var(--accent-hover);
}
}
}

View File

@@ -0,0 +1,172 @@
import { Component, OnInit, inject, signal } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { SeoService } from '@core/services/seo.service';
interface ProjectFeature {
icon: string;
title: string;
description: string;
}
interface Project {
slug: string;
company: string;
branch: string;
image: string;
galleryImages: string[];
shortDescription: string;
description: string;
features: ProjectFeature[];
result?: string;
testimonial?: {
quote: string;
author: string;
role: string;
};
}
const PROJECTS: Project[] = [
{
slug: 'metzgerei-schlachthof-qualitaet',
company: 'Metzgerei Schlachthof-Qualität',
branch: 'Fleischerei & Metzgerei',
image: '/images/projekte/metzgerei.jpg',
galleryImages: [
'/images/projekte/metzgerei.jpg',
'/images/projekte/metzgerei-detail1.jpg',
'/images/projekte/metzgerei-detail2.jpg'
],
shortDescription: 'Premium-Webauftritt für traditionelle Metzgerei mit Fleischerei in der Region',
description: 'Eine traditionelle Metzgerei mit jahrzehntelanger Erfahrung wollte ihre handwerkliche Qualität auch online sichtbar machen. Wir haben einen Webauftritt geschaffen, der die Wertigkeit ihrer Produkte widerspiegelt modern, aber mit einem Hauch von Tradition.',
features: [
{
icon: 'search',
title: 'SEO-Optimierung',
description: 'Auffindbar bei Google für regionale Suchbegriffe wie "Metzgerei in der Region" und "Frisches Fleisch online".'
},
{
icon: 'smartphone',
title: 'Responsive Design',
description: 'Perfekte Darstellung auf allen Geräten vom Smartphone beim Einkauf bis zum Desktop.'
},
{
icon: 'shield',
title: 'DSGVO-konform',
description: 'Vollständig rechtssicher mit Cookie-Banner, Impressum und Datenschutzerklärung nach aktuellen Standards.'
}
],
result: '+40% mehr Anfragen über Website',
testimonial: {
quote: 'Endlich eine Webseite, die unsere handwerkliche Qualität widerspiegelt. Die Zusammenarbeit war professionell und unkompliziert.',
author: 'Thomas B.',
role: 'Inhaber'
}
},
{
slug: 'finanzberatung-vermoegenswert',
company: 'Finanzberatung Vermögenswert',
branch: 'Finanzdienstleistung',
image: '/images/projekte/finanzberatung.jpg',
galleryImages: [
'/images/projekte/finanzberatung.jpg',
'/images/projekte/finanzberatung-detail1.jpg',
'/images/projekte/finanzberatung-detail2.jpg'
],
shortDescription: 'Vertrauenswürdiger Online-Auftritt für unabhängige Finanzberatung',
description: 'Als unabhängige Finanzberatung ist Vertrauen das wichtigste Gut. Wir haben eine Website entwickelt, die Kompetenz und Seriosität vermittelt, ohne dabei steif oder unpersönlich zu wirken.',
features: [
{
icon: 'target',
title: 'Lead-Generierung',
description: 'Strategisch platzierte Call-to-Actions und ein optimiertes Kontaktformular für qualifizierte Anfragen.'
},
{
icon: 'calendar',
title: 'Terminbuchung',
description: 'Online-Terminvereinbarung direkt über die Website rund um die Uhr, ohne telefonische Hürden.'
},
{
icon: 'award',
title: 'Premium-Design',
description: 'Hochwertige Optik, die das Qualitätsversprechen der Beratung visuell unterstreicht.'
}
],
result: '+60% neue Terminbuchungen',
testimonial: {
quote: 'Unsere neuen Kunden sagen oft, dass sie uns wegen der professionellen Website kontaktiert haben. Das zeigt, wie wichtig ein guter erster Eindruck ist.',
author: 'Michael S.',
role: 'Geschäftsführer'
}
},
{
slug: 'physiotherapie-beweglich',
company: 'Physiotherapie Beweglich',
branch: 'Gesundheitswesen',
image: '/images/projekte/physiotherapie.jpg',
galleryImages: [
'/images/projekte/physiotherapie.jpg',
'/images/projekte/physiotherapie-detail1.jpg',
'/images/projekte/physiotherapie-detail2.jpg'
],
shortDescription: 'Moderne Praxis-Website für Physiotherapie und Rehabilitation',
description: 'Eine modern aufgestellte Physiotherapie-Praxis mit focus auf ganzheitliche Behandlungsmethoden. Die Website sollte Patienten dabei helfen, die richtige Behandlung zu finden und einfach einen Termin zu buchen.',
features: [
{
icon: 'calendar-check',
title: 'Online-Terminbuchung',
description: 'Intuitives Buchungssystem mit Kalenderansicht und automatischen Erinnerungen per E-Mail.'
},
{
icon: 'list',
title: 'Leistungsübersicht',
description: 'Übersichtliche Darstellung aller Behandlungsangebote mit detaillierten Beschreibungen.'
},
{
icon: 'google',
title: 'Google-optimiert',
description: 'Lokale SEO-Strategie für Top-Platzierungen bei Suchbegriffen wie "Physiotherapie" in der Umgebung.'
}
],
result: 'Top 3 bei Google-Suche',
testimonial: {
quote: 'Wir werden regelmäßig für unsere moderne Website gelobt. Viele Patienten buchen sogar direkt online das spart uns Zeit.',
author: 'Sarah M.',
role: 'Praxisinhaberin'
}
}
];
@Component({
selector: 'app-project-detail',
imports: [RouterLink],
templateUrl: './project-detail.component.html',
styleUrl: './project-detail.component.scss',
})
export class ProjectDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
private seo = inject(SeoService);
project = signal<Project | null>(null);
otherProjects = signal<Project[]>([]);
ngOnInit(): void {
const slug = this.route.snapshot.paramMap.get('slug');
const project = PROJECTS.find(p => p.slug === slug) || null;
this.project.set(project);
this.otherProjects.set(PROJECTS.filter(p => p.slug !== slug));
if (project) {
this.seo.updateMetadata({
title: `${project.company} | Hurler Webdesign`,
description: project.shortDescription,
socialsDescription: `Erfahren Sie, wie wir ${project.company} zu mehr Erfolg im Internet verholfen haben.`,
type: 'website'
});
}
}
getProjectBySlug(slug: string): Project | undefined {
return PROJECTS.find(p => p.slug === slug);
}
}

1
tsconfig.tsbuildinfo Normal file
View File

@@ -0,0 +1 @@
{"fileNames":[],"fileInfos":[],"root":[],"options":{"experimentalDecorators":true,"importHelpers":true,"module":200,"noFallthroughCasesInSwitch":true,"noImplicitOverride":true,"noImplicitReturns":true,"noPropertyAccessFromIndexSignature":true,"skipLibCheck":true,"strict":true,"target":9},"version":"5.9.3"}