contact row
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
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">
|
<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 />
|
||||||
|
|||||||
Reference in New Issue
Block a user