diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 4e47029..12fe626 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -17,3 +17,16 @@ export const socialIcons = { linkedin: "https://www.linkedin.com/company/kontextwerk", youtube: "https://www.youtube.com/@kontextwerk", } + +export type HeaderLinkType = "link" | "button" + +export type HeaderLink = + | ({ sectionId: string; href?: never } & { label: string; type?: HeaderLinkType }) + | ({ href: string; sectionId?: never } & { label: string; type?: HeaderLinkType }) + +export const headerLinks: HeaderLink[] = [ + { label: "Überblick", sectionId: "leistungen" }, + { label: "Voicebot", sectionId: "voicebot-demo" }, + { label: "Chatbot", sectionId: "chatbot-demo" }, + { label: "Kontakt", sectionId: "kontakt", type: "button" }, +] diff --git a/frontend/src/lib/actions.ts b/frontend/src/lib/actions.ts index 2c8dbb5..6c6075b 100644 --- a/frontend/src/lib/actions.ts +++ b/frontend/src/lib/actions.ts @@ -15,7 +15,8 @@ export const spaLink: Action = (node) => { const currentUrl = window.location.pathname + window.location.search + window.location.hash if (nextUrl === currentUrl) { - window.scrollTo({ top: 0, behavior: "smooth" }) + const hashValue = anchor.hash ? anchor.hash.substring(1) : null + scrollToTarget(hashValue) return } @@ -32,14 +33,48 @@ export const spaLink: Action = (node) => { } } +const scrollToTarget = (hash: string | null) => { + const header = document.getElementById("header-container") + const headerHeight = header?.getBoundingClientRect().height ?? 0 + + if (!hash) { + window.scrollTo({ top: 0, behavior: "smooth" }) + return + } + + const decodedHash = decodeURIComponent(hash) + + const tryScroll = (attempt = 0) => { + const target = document.getElementById(decodedHash) + + if (target) { + const targetPosition = target.getBoundingClientRect().top + window.scrollY + const offset = Math.max(targetPosition - headerHeight - 16, 0) + window.scrollTo({ top: offset, behavior: "smooth" }) + return + } + + if (attempt < 10) { + requestAnimationFrame(() => tryScroll(attempt + 1)) + } else { + window.scrollTo({ top: 0, behavior: "smooth" }) + } + } + + requestAnimationFrame(() => tryScroll()) +} + export const spaNavigate = (to: string, options?: { replace?: boolean }) => { + const hashIndex = to.indexOf("#") + const hash = hashIndex >= 0 ? to.slice(hashIndex + 1) : null + if (options?.replace) { window.history.replaceState(null, "", to) } else { window.history.pushState(null, "", to) } - window.scrollTo({ top: 0, behavior: "smooth" }) + scrollToTarget(hash) } export const spaBack = () => { diff --git a/frontend/src/lib/components/header/Header.svelte b/frontend/src/lib/components/header/Header.svelte index cbe07a6..a5a6abc 100644 --- a/frontend/src/lib/components/header/Header.svelte +++ b/frontend/src/lib/components/header/Header.svelte @@ -1,9 +1,13 @@
+
@@ -92,6 +115,80 @@ object-fit: contain; } } + .nav-links { + display: flex; + align-items: center; + gap: 1.5rem; + list-style: none; + margin: 0; + padding: 0; + li { + display: flex; + align-items: center; + justify-content: center; + } + .nav-link { + color: var(--text-100); + text-decoration: none; + font-weight: 500; + font-size: 0.95rem; + transition: + color 0.2s ease, + background-color 0.2s ease, + border-color 0.2s ease; + padding: 0.5rem 0; + position: relative; + + &:focus-visible { + outline: 2px solid var(--primary-200); + outline-offset: 3px; + } + + &:after { + content: ""; + position: absolute; + left: 0; + bottom: 0; + width: 100%; + height: 2px; + background: currentColor; + transform: scaleX(0); + transform-origin: left; + transition: transform 0.2s ease; + } + + &:hover, + &:focus-visible { + color: var(--primary-200); + + &:after { + transform: scaleX(1); + } + } + } + + .nav-link-button { + border: 1px solid var(--primary-200); + border-radius: 999px; + padding: 0.45rem 1.4rem; + font-weight: 600; + color: var(--neutral-white); + background: linear-gradient(135deg, var(--primary-200), var(--primary-100)); + box-shadow: 0 0 12px rgba(116, 30, 32, 0.45); + + &:after { + display: none; + } + transition: + background 0.3s ease, + color 0.3s ease; + &:hover, + &:focus-visible { + color: var(--neutral-white); + background: linear-gradient(135deg, var(--primary-100), #ff5252); + } + } + } } } &.homepageHeader { @@ -102,4 +199,35 @@ background-color: var(--bg-100); } } + + @media @mobile { + .headercontainer { + .padding { + .menu { + flex-direction: column; + height: auto; + padding: 0.8rem 0; + gap: 0.8rem; + .logo-container { + height: 48px; + img { + height: 44px; + } + } + .nav-links { + flex-wrap: wrap; + justify-content: center; + gap: 0.8rem 1.2rem; + .nav-link { + padding: 0.4rem 0; + font-size: 0.9rem; + } + .nav-link-button { + padding: 0.4rem 1rem; + } + } + } + } + } + } diff --git a/frontend/src/lib/components/staticPageRows/ChatbotPreview.svelte b/frontend/src/lib/components/staticPageRows/ChatbotPreview.svelte index 5e81c88..0b14171 100644 --- a/frontend/src/lib/components/staticPageRows/ChatbotPreview.svelte +++ b/frontend/src/lib/components/staticPageRows/ChatbotPreview.svelte @@ -46,6 +46,7 @@ properties={voiceProperties} title="Chatbot Demo" reverse={true} + sectionId="chatbot-demo" upperDescription="Unsere Voicebots sind rund um die Uhr für Ihre Kunden da und bieten maßgeschneiderte Lösungen, die perfekt auf Ihre Bedürfnisse abgestimmt sind." lowerDescription="Durch den Einsatz modernster KI-Technologien gewährleisten wir eine intelligente und effiziente Kommunikation, die den höchsten Datenschutzstandards entspricht." > diff --git a/frontend/src/lib/components/staticPageRows/ContactFormRow.svelte b/frontend/src/lib/components/staticPageRows/ContactFormRow.svelte index fac221c..dd5e7ea 100644 --- a/frontend/src/lib/components/staticPageRows/ContactFormRow.svelte +++ b/frontend/src/lib/components/staticPageRows/ContactFormRow.svelte @@ -84,7 +84,10 @@ {#snippet contentSnippet()} -
+
Kontakt diff --git a/frontend/src/lib/components/staticPageRows/CoreSellingPoints.svelte b/frontend/src/lib/components/staticPageRows/CoreSellingPoints.svelte index adeb04a..739e336 100644 --- a/frontend/src/lib/components/staticPageRows/CoreSellingPoints.svelte +++ b/frontend/src/lib/components/staticPageRows/CoreSellingPoints.svelte @@ -46,28 +46,28 @@ title: "Schneller", alias: "Schneller", shortDescription: - "Unser internes System sorgt für eine schnelle und effiziente Umsetzung Ihres Projekts. Dadurch ermöglichen wir, ihr Projekt in Wochen, statt Monaten zu realisieren!", + "Projektstart innerhalb von 3 Werktagen. Ein FAQ-Bot ist nach 3 Tagen live, komplexere Lösungen in Rekordzeit – möglich durch erprobte Prozesse und eine modulare Codebasis.", color: "#ffffff", }, { - title: "Qualitativer", - alias: "Qualitativer", + title: "Zuverlässiger", + alias: "Zuverlässiger", shortDescription: - "Höhere Qualität durch spezialisierte Experten. Wir setzen auf ein Netzwerk aus erfahrenen Fachleuten, um Ihnen die bestmöglichen Lösungen mit State-of-the-Art-Technologien zu bieten.", + "Der Assistent greift ausschließlich auf freigegebenes Unternehmenswissen zu. Mehrstufige Qualitätsprüfungen und monatliche KPI-Berichte sichern dauerhaft verlässliche Ergebnisse.", color: "#741e20", }, { - title: "Entspannter", - alias: "Entspannter", + title: "Sorglos", + alias: "Sorglos", shortDescription: - "Wir bieten Ihnen einen Rundum-sorglos-Service. Von der Konzeption über die Umsetzung bis hin zur Nachbetreuung. Alles aus einer Hand.", + "Alles aus einer Hand: Kick-off-Workshop, Datenaufbereitung, Implementierung und Kanalintegration. Hosting im deutschen Rechenzentrum, 24/7-Monitoring, automatisierte Tests und kontinuierliche Optimierung sind für uns Standard.", color: "#ffffff", }, { - title: "Autonomer", - alias: "Autonomer", + title: "Unabhängiger", + alias: "Unabhängiger", shortDescription: - "Sie entscheiden wo die Software gehostet wird. Ob bei uns in unseren deutschen Rechenzentren oder bei Ihnen, wir bieten beides an.", + "Betriebsmodell nach Wahl: Managed in deutschen Rechenzentren, Private Cloud oder On-Premise. Offene Schnittstellen und containerisierte Übergabe garantieren volle Kontrolle – auf Wunsch auch mit Ihrem eigenen Sprachmodell.", color: "#741e20", }, ] @@ -75,6 +75,7 @@
diff --git a/frontend/src/lib/components/staticPageRows/VoicebotPreview.svelte b/frontend/src/lib/components/staticPageRows/VoicebotPreview.svelte index 6b7d1e4..02af7c6 100644 --- a/frontend/src/lib/components/staticPageRows/VoicebotPreview.svelte +++ b/frontend/src/lib/components/staticPageRows/VoicebotPreview.svelte @@ -53,6 +53,7 @@ diff --git a/frontend/src/lib/components/widgets/MediaQuery.svelte b/frontend/src/lib/components/widgets/MediaQuery.svelte deleted file mode 100644 index d529ce7..0000000 --- a/frontend/src/lib/components/widgets/MediaQuery.svelte +++ /dev/null @@ -1,53 +0,0 @@ - - -{#if matches} - -{/if} diff --git a/frontend/src/lib/components/widgets/MedialibFile.svelte b/frontend/src/lib/components/widgets/MedialibFile.svelte deleted file mode 100644 index 189820d..0000000 --- a/frontend/src/lib/components/widgets/MedialibFile.svelte +++ /dev/null @@ -1,83 +0,0 @@ - - - - -{#if id} - {#if loading} - {#if !noPlaceholder} - {@render loadingSnippet({ entry, src: fileSrc })} - {/if} - {:else if entry} - {@render childrenSnippet({ entry, src: fileSrc })} - {:else if !noPlaceholder} - {@render notFoundSnippet()} - {/if} -{/if} diff --git a/frontend/src/lib/components/widgets/MedialibImage.svelte b/frontend/src/lib/components/widgets/MedialibImage.svelte deleted file mode 100644 index dde9717..0000000 --- a/frontend/src/lib/components/widgets/MedialibImage.svelte +++ /dev/null @@ -1,138 +0,0 @@ - - -{#if id} - - {#snippet childrenSnippet({ entry, src }: { entry: MedialibEntry; src: string })} - {entry.alt - {/snippet} - {#snippet loadingSnippet({ entry, src }: { entry: MedialibEntry; src: string })}{/snippet} - - {#snippet notFoundSnippet()}{/snippet} - -{/if} diff --git a/frontend/src/lib/components/widgets/ProductCategoryFrame.svelte b/frontend/src/lib/components/widgets/ProductCategoryFrame.svelte index 159d654..6c4f989 100644 --- a/frontend/src/lib/components/widgets/ProductCategoryFrame.svelte +++ b/frontend/src/lib/components/widgets/ProductCategoryFrame.svelte @@ -7,7 +7,7 @@ upperDescription, lowerDescription, reverse = false, - + sectionId, primaryContent, }: { properties: Array<{ title: string; icon: string; color: string }> @@ -15,12 +15,14 @@ upperDescription: string reverse?: boolean lowerDescription: string + sectionId?: string primaryContent: () => any } = $props()