contact row

This commit is contained in:
2025-10-05 18:36:29 +00:00
parent 740cb3e1e6
commit 23f8bc2491
4 changed files with 426 additions and 1 deletions

View File

@@ -21,7 +21,7 @@
}) })
</script> </script>
<CrinkledSection> <CrinkledSection brightBackground={false}>
{#snippet contentSnippet()} {#snippet contentSnippet()}
<footer class="footer"> <footer class="footer">
<section id="legal-section"> <section id="legal-section">

View File

@@ -0,0 +1,277 @@
<script lang="ts">
import CrinkledSection from "../CrinkledSection.svelte"
import Input from "../widgets/Input.svelte"
import { apiBaseURL } from "../../../config"
let firstName = ""
let lastName = ""
let message = ""
let submitting = false
let responseMessage = ""
let responseType: "success" | "error" | "" = ""
const clearFeedback = () => {
responseMessage = ""
responseType = ""
}
const updateField = (setter: (value: string) => void) => (event: Event) => {
const target = event.currentTarget as HTMLInputElement | HTMLTextAreaElement
setter(target.value)
if (responseMessage) {
clearFeedback()
}
}
const onFirstNameChange = updateField((value) => {
firstName = value
})
const onLastNameChange = updateField((value) => {
lastName = value
})
const onMessageChange = updateField((value) => {
message = value
})
const submitContact = async () => {
if (submitting) return
clearFeedback()
if (!firstName.trim() || !lastName.trim() || !message.trim()) {
responseMessage = "Bitte füllen Sie alle Felder aus."
responseType = "error"
return
}
submitting = true
try {
const response = await fetch(`${apiBaseURL}contact`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
firstName: firstName.trim(),
lastName: lastName.trim(),
message: message.trim(),
}),
})
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`)
}
responseMessage = "Vielen Dank! Wir melden uns zeitnah bei Ihnen."
responseType = "success"
firstName = ""
lastName = ""
message = ""
} catch (error) {
console.error("Failed to submit contact request", error)
responseMessage = "Leider konnte die Anfrage nicht gesendet werden. Bitte versuchen Sie es erneut."
responseType = "error"
} finally {
submitting = false
}
}
</script>
<CrinkledSection>
{#snippet contentSnippet()}
<section class="small-wrapper contact-row">
<div class="contact-wrapper">
<div class="copy">
<small>Kontakt</small>
<h2>Kontaktieren Sie uns</h2>
<p>
Ob Fragen zu unseren Lösungen oder konkrete Projektideen schreiben Sie uns und wir melden uns
schnellstmöglich bei Ihnen.
</p>
</div>
<form
class="contact-form"
on:submit|preventDefault={submitContact}
>
<div class="row">
<Input
id="contact-first-name"
name="firstName"
type="text"
placeholder="Vorname"
bind:value={firstName}
onChange={onFirstNameChange}
disabled={submitting}
classList="contact-input"
/>
</div>
<div class="row">
<Input
id="contact-last-name"
name="lastName"
type="text"
placeholder="Nachname"
bind:value={lastName}
onChange={onLastNameChange}
disabled={submitting}
classList="contact-input"
/>
</div>
<div class="row">
<Input
id="contact-message"
name="message"
type="textarea"
placeholder="Nachricht"
bind:value={message}
onChange={onMessageChange}
disabled={submitting}
classList="contact-input"
/>
</div>
{#if responseMessage}
<p
class={`status ${responseType}`}
role={responseType === "error" ? "alert" : "status"}
aria-live="polite"
>
{responseMessage}
</p>
{/if}
<button
class="cta primary"
type="submit"
disabled={submitting}
>
{submitting ? "Wird gesendet…" : "Absenden"}
</button>
</form>
</div>
</section>
{/snippet}
</CrinkledSection>
<style lang="less">
@import "../../assets/css/variables.less";
.contact-row {
width: 100%;
background-color: white;
.contact-wrapper {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 2.4rem;
width: 100%;
overflow: visible;
}
.copy {
display: flex;
flex-direction: column;
gap: 0.8rem;
color: var(--text-invers-100);
h2 {
margin: 0;
color: inherit;
}
p {
margin: 0;
}
small {
font-size: 0.875rem;
letter-spacing: 0.08rem;
text-transform: uppercase;
font-weight: 600;
}
}
.contact-form {
display: flex;
flex-direction: column;
gap: 1rem;
color: var(--text-invers-100);
.row {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.status {
margin: 0;
font-weight: 600;
&.success {
color: var(--text-200);
}
&.error {
color: var(--primary-100);
}
}
button {
align-self: flex-start;
}
}
}
:global(.contact-form label) {
display: flex;
flex-direction: column;
gap: 0.4rem;
color: var(--text-invers-100);
}
:global(.contact-form label > span:not(.underline)) {
font-weight: 600;
}
:global(.contact-form .contact-input) {
width: 100%;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
border: 1px solid var(--text-invers-100);
background-color: transparent;
color: var(--text-invers-100);
font: inherit;
transition: border-color 0.2s ease;
}
:global(.contact-form textarea.contact-input) {
min-height: 8rem;
resize: vertical;
}
:global(.contact-form .contact-input:focus) {
outline: 2px solid var(--text-invers-150);
outline-offset: 2px;
}
:global(.contact-form .contact-input:disabled) {
cursor: not-allowed;
opacity: 0.6;
}
:global(.contact-form .underline) {
display: none;
}
@media @mobile {
.contact-row {
.contact-form {
button {
width: 100%;
}
}
}
}
</style>

View File

@@ -0,0 +1,146 @@
<script lang="ts">
import { mdiInformationOutline } from "@mdi/js"
import Icon from "./Icon.svelte"
import { tooltip } from "../../functions/utils"
export let value: any,
id,
classList: string = "",
onChange: (e: Event) => void,
type: "password" | "text" | "number" | "checkbox" | "noInput" | "textarea" | "select" = "text",
placeholder: string = "",
disabled: boolean = false,
name = "",
options: { name: string; value: string }[] = [],
selectedOptionIndex = 0,
helperText = ""
$: hasValue = Boolean(value)
const attributes = {
id,
class: classList,
placeholder,
name: name || id,
disabled: !!disabled,
}
</script>
<label
style=""
class:textarea={type == "textarea"}
class:checkbox={type == "checkbox"}
>
{#if type !== "checkbox"}
<span class:hasValue={hasValue || type === "noInput"}>{placeholder}</span>
{/if}
{#if type == "checkbox"}
<input
type="checkbox"
{...attributes}
onchange={onChange}
bind:checked={value}
/>
<button
type="button"
class="checkit-span"
aria-label="Toggle checkbox"
tabindex={0}
onclick={() => {
value = !value
setTimeout(() => {
const event = new Event("change", { bubbles: true })
document.getElementById(id)?.dispatchEvent(event)
}, 10)
}}
onkeydown={(e) => {}}
></button>
{/if}
{#if type == "password"}
<input
{...attributes}
onblur={onChange}
bind:value
onchange={onChange}
type="password"
class="sentry-mask"
/>
{/if}
{#if type == "text"}
<input
onblur={onChange}
{...attributes}
bind:value
onchange={onChange}
/>
{/if}
{#if type == "textarea"}
<textarea
onblur={onChange}
{...attributes}
bind:value
onchange={onChange}
></textarea>
{/if}
{#if type == "number"}
<input
onblur={onChange}
type="number"
{...attributes}
bind:value
onchange={onChange}
/>
{/if}
{#if type == "noInput"}
<div class="no-input">
{#if id.includes("pass")}
************
{:else}
{Boolean(value) ? value : "-"}
{/if}
</div>
{/if}
{#if type == "select"}
<select
onchange={onChange}
{...attributes}
bind:value
>
{#each options as option, index}
<option
value={option.value}
selected={index === selectedOptionIndex}
>
{option.name}
</option>
{/each}
</select>
{/if}
{#if type !== "checkbox"}
<span class="underline"></span>
{/if}
{#if helperText}
<div
use:tooltip={{
content: helperText,
}}
class="helperText"
>
<Icon path={mdiInformationOutline} />
</div>
{/if}
</label>
<style lang="less">
.checkbox {
width: 1.2rem !important;
}
.helperText {
position: absolute;
right: 0px;
color: var(--bg-100);
top: 50%;
transform: translateY(-50%);
}
</style>

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import ChatbotPreview from "../lib/components/staticPageRows/ChatbotPreview.svelte" import ChatbotPreview from "../lib/components/staticPageRows/ChatbotPreview.svelte"
import ContactFormRow from "../lib/components/staticPageRows/ContactFormRow.svelte"
import CoreSellingPoints from "../lib/components/staticPageRows/CoreSellingPoints.svelte" import CoreSellingPoints from "../lib/components/staticPageRows/CoreSellingPoints.svelte"
import VoicebotPreview from "../lib/components/staticPageRows/VoicebotPreview.svelte" import VoicebotPreview from "../lib/components/staticPageRows/VoicebotPreview.svelte"
</script> </script>
@@ -8,3 +9,4 @@
<VoicebotPreview /> <VoicebotPreview />
<ChatbotPreview /> <ChatbotPreview />
<ContactFormRow />