contact row
This commit is contained in:
@@ -21,7 +21,7 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<CrinkledSection>
|
||||
<CrinkledSection brightBackground={false}>
|
||||
{#snippet contentSnippet()}
|
||||
<footer class="footer">
|
||||
<section id="legal-section">
|
||||
|
||||
277
frontend/src/lib/components/staticPageRows/ContactFormRow.svelte
Normal file
277
frontend/src/lib/components/staticPageRows/ContactFormRow.svelte
Normal 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>
|
||||
146
frontend/src/lib/components/widgets/Input.svelte
Normal file
146
frontend/src/lib/components/widgets/Input.svelte
Normal 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>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
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 VoicebotPreview from "../lib/components/staticPageRows/VoicebotPreview.svelte"
|
||||
</script>
|
||||
@@ -8,3 +9,4 @@
|
||||
|
||||
<VoicebotPreview />
|
||||
<ChatbotPreview />
|
||||
<ContactFormRow />
|
||||
|
||||
Reference in New Issue
Block a user