dml_frontend/components/ContactForm.vue

531 lines
15 KiB
Vue

<template>
<div
class="contactBubble"
:class="{ active: isContactBubbleOpen }"
:aria-hidden="!isContactBubbleOpen"
:aria-expanded="isContactBubbleOpen"
aria-labelledby="controlIcon"
role="dialog"
>
<svg
id="controlIcon"
tabindex="0"
role="button"
aria-label="Toggle contact form"
@click="toggleContactBubble"
@keydown.space.prevent="toggleContactBubble"
@keydown.enter.prevent="toggleContactBubble"
>
<use :xlink:href="`/assets/icons/collection.svg#${isContactBubbleOpen ? 'times' : 'talk'}`" />
</svg>
<div
v-show="isContactBubbleOpen"
class="contactContainer"
role="form"
aria-labelledby="contactTitle"
>
<div class="row left m-2">
<!-- Linke Seite -->
<div id="hintBox" class="col-md-6">
<NuxtImg
v-if="screenWidth <= 768"
class="mobileAspBox"
:src="companyinfo?.profileImage?.data?.attributes?.url"
alt="Sabrina Hennrich"
:width="400"
format="webp"
provider="strapi"
loading="lazy"
/>
<h2 id="contactTitle">{{ $t('contactForm.yourcontact2us') }}</h2>
<p class="my-4">
<svg aria-hidden="true">
<use xlink:href="/assets/icons/collection.svg#phone" />
</svg>
<span>{{ companyinfo.phone }}</span>
</p>
<div v-if="screenWidth > 768" class="pt-3">
<h3>{{ $t('contactForm.ourOffice') }}</h3>
<p class="address">
{{ companyinfo.company }}<br >
{{ companyinfo.street }} <br >
{{ companyinfo.postalcode }} {{ companyinfo.city }}
</p>
<p class="aspProf">{{ $t('contactForm.yourcontactperson') }} <b>Sabrina Hennrich</b></p>
<div class="aspBox">
<NuxtImg
:src="companyinfo?.profileImage?.data?.attributes?.url"
alt="Ansprechpartner Sabrina Hennrich"
:width="150"
format="webp"
provider="strapi"
loading="lazy"
/>
</div>
</div>
</div>
<!-- Rechte Seite -->
<div class="col-md-6">
<div v-if="!formSent">
<form novalidate @submit.prevent="submitForm">
<div class="form-group">
<label for="name">{{ $t('contactForm.name') }}</label>
<input
id="name"
v-model="form.name"
class="form-control"
type="text"
name="name"
ref="firstInput"
required
autocomplete="name"
:aria-invalid="!!errors.name"
:aria-describedby="errors.name ? 'error-name' : null"
@blur="validateName"
>
<span
v-if="errors.name"
id="error-name"
class="error"
role="alert"
>
{{ errors.name }}
</span>
</div>
<div class="form-group">
<label for="email">{{ $t('contactForm.email') }}</label>
<input
id="email"
v-model="form.email"
class="form-control"
type="email"
name="email"
required
autocomplete="email"
:aria-invalid="!!errors.email"
:aria-describedby="errors.email ? 'error-email' : null"
@blur="validateEmail"
>
<span
v-if="errors.email"
id="error-email"
class="error"
role="alert"
>
{{ errors.email }}
</span>
</div>
<div class="form-group">
<label for="phone">{{ $t('contactForm.phone') }}</label>
<input
id="phone"
v-model="form.phone"
class="form-control"
type="tel"
name="phone"
autocomplete="tel"
:aria-invalid="!!errors.phone"
:aria-describedby="errors.phone ? 'error-phone' : null"
@blur="validatePhone"
>
<span
v-if="errors.phone"
id="error-phone"
class="error"
role="alert"
>
{{ errors.phone }}
</span>
</div>
<div class="form-group">
<label for="message">{{ $t('contactForm.message') }}</label>
<textarea
id="message"
v-model="form.message"
class="form-control mt-4"
name="message"
rows="4"
required
/>
</div>
<p class="smallText">
<span class="check"></span>
{{ $t('contactForm.privacyInfotextBeforeLink') }}
<NuxtLinkLocale
:to="'privacy'"
:aria-label="$t('privacy')"
>
{{ $t('contactForm.privacyInfotextLinkText') }}
</NuxtLinkLocale>
</p>
<button
type="submit"
class="pinkBtn"
:aria-label="$t('contactForm.sendMessage')"
>
{{ $t('contactForm.sendMessage') }}
</button>
</form>
</div>
<!-- Dankeschön-Text -->
<div v-else class="mt-5 thx">
<h3 class="pt-5">{{ $t('contactForm.confirmation.thx') }}</h3>
<p>{{ $t('contactForm.confirmation.info') }}</p>
<p>{{ $t('contactForm.confirmation.salutation') }}</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useMainStore } from '@/stores/main';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
const firstInput = ref<HTMLInputElement | null>(null);
// i18n Setup
const { t } = useI18n();
// Zugriff auf den Pinia-Store
const mainStore = useMainStore();
const { companyinfo } = storeToRefs(mainStore);
const isContactBubbleOpen = computed(() => mainStore.contactBoxOpen);
const toggleContactBubble = () => mainStore.toggleContactBubble();
const screenWidth = computed(() => mainStore.screenWidth);
const formSent = ref(false);
// Formular- und Fehlerzustand
const form = reactive({
name: '',
email: '',
phone: '',
message: '',
company: '',
language: 'de' // Beispielhaft festgelegt
});
const errors = reactive({
name: null as string | null,
email: null as string | null,
phone: null as string | null
});
// Validierungsfunktionen
const validateName = () => {
errors.name = form.name ? null : t('contactForm.validation.nameRequired');
};
const validateEmail = () => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!form.email && !form.phone) {
errors.email = t('contactForm.validation.emailOrPhoneRequired');
} else if (form.email && !emailRegex.test(form.email)) {
errors.email = t('contactForm.validation.invalidEmail');
} else {
errors.email = null;
}
};
const validatePhone = () => {
const phoneRegex = /^[+]?([0-9\- ]{6,15})$/;
if (!form.email && !form.phone) {
errors.phone = t('contactForm.validation.emailOrPhoneRequired');
} else if (form.phone && !phoneRegex.test(form.phone)) {
errors.phone = t('contactForm.validation.invalidPhone');
} else {
errors.phone = null;
}
};
const validateForm = () => {
validateName();
validateEmail();
validatePhone();
};
const submitForm = async () => {
validateForm();
if (!errors.name && !errors.email && !errors.phone) {
try {
await mainStore.sendContactRequestToCMS({
name: form.name,
email: form.email,
phone: form.phone,
message: form.message,
company: form.company,
language: navigator.language,
page: window.location.pathname
});
formSent.value = true;
setTimeout(() => {
formSent.value = false;
}, 5500);
resetForm();
} catch (error) {
console.error('Fehler beim Senden des Formulars:', error);
alert(t('contactForm.errorMessage'));
}
}
};
const resetForm = () => {
form.name = '';
form.email = '';
form.phone = '';
form.message = '';
form.company = '';
errors.name = null;
errors.email = null;
errors.phone = null;
};
// ESC & Fokusmanagement
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isContactBubbleOpen.value) {
toggleContactBubble();
}
};
// Füge den EventListener beim Mount hinzu
onMounted(() => {
window.addEventListener('keydown', handleKeydown);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', handleKeydown);
});
// Wenn die Bubble geöffnet wird: Fokus setzen
watch(isContactBubbleOpen, async (newVal) => {
if (newVal) {
await nextTick()
firstInput.value?.focus()
}
})
</script>
<style lang="sass">
@keyframes awarePulse
0%
box-shadow: 0 0 0 0 rgba(202, 103, 133, 0.25)
50%
box-shadow: 0 0 0 25px rgba(202, 103, 133, 0.15)
100%
box-shadow: 0 0 0 40px rgba(202, 103, 133, 0)
.contactBubble
position: fixed
bottom: 3rem
right: 2rem
width: 80px
height: 80px
background-color: $pink
border-radius: $loopShape
color: white
font-size: 2rem
text-align: center
z-index: 100
cursor: pointer
transition: .8s
display: flex
flex-direction: column
align-items: center
justify-content: center
animation: awarePulse 1.8s infinite ease-out
@media(max-width: $breakPointMD)
bottom: 4vw
right: 4vw
.contactContainer
.thx
background-color: $lightgrey
border-radius: $loopShape
padding: 0 3rem 2rem
text-align: center
h3
font-size: 1.2rem
p
font-size: 1rem
#hintBox
padding-top: 3rem
h2
font-size: 1.8rem
@media(max-width: $breakPointMD)
text-align: center
padding-top: 1rem
svg
max-height: 60px
width: 80%
margin: 6rem 10%
h3
font-size: .8rem
font-family: 'Mainfont-Bold'
color: $pink
p
color: $darkgrey
font-size: 1.2rem
&.smallText
font-size: .8rem
&.address
font-size: .9rem
.check
font-size: 1.4rem
color: $pink
svg
width: 2rem
height: 2rem
fill: $darkgrey
margin-right: 1rem
max-height: 100px
@media(max-width: $breakPointSM)
margin-right: .5rem
width: 1.5rem
height: 1.5rem
a
color: $primaryColor
#controlIcon
position: absolute
bottom: 50%
left: 50%
transform: translate(-50%, 50%)
fill: white
height: auto
width: 3rem
height: 3rem
z-index: 101
&.active
height: 90vh
width: 90vw
background-color: rgba(lighten($beige, 8%), .98)
color: $darkgrey
display: flex
flex-direction: column
text-align: left
border: 1px solid $lightgrey
animation: none
box-shadow: 1px 1px 15px 2px $beige
@media(max-width: $breakPointMD)
border-radius: 0
height: 100%
width: 100%
right: 0
top: 0
#controlIcon
right: 5vw
left: auto
bottom: 4rem !important
#controlIcon
bottom: 3rem
fill: #888
width: 2rem
.mobileAspBox
width: 30vw
max-width: 90px
float: left
margin: 0 .5rem 1rem -2rem
border-radius: $loopShape
@media(max-width: $breakPointSM)
margin-right: .1rem
input, textarea
all: unset
position: relative
width: 100%
padding: .2rem .5rem .2rem .5rem
font-size: 1.2rem
background-color: transparent
border-top-right-radius: .3rem
border-top-left-radius: .3rem
border-bottom: 1px solid lighten($darkgrey, 40%)
border-left: none
border-right: none
border-top: none
margin-bottom: .5rem
margin-top: 0
&::placeholder
color: darken($beige, 8%)
font-size: 1.1rem
&:focus
outline: none // Entfernt den Standard-Fokusrahmen
border-bottom-color: $primaryColor // Ändert die Farbe der unteren Begrenzung
transition: border-color 0.3s ease-in-out // Smooth Transition beim Wechsel der Farbe
&:-webkit-autofill
background-color: transparent !important
color: inherit !important
-webkit-box-shadow: 0 0 0px 1000px transparent inset
box-shadow: 0 0 0px 1000px transparent inset
transition: background-color 5000s ease-in-out 0s
textarea
height: 20%
font-size: 1.1rem
button
font-size: 1.2rem
border: none
background-image: linear-gradient(to bottom right, lighten($pink, 10%), $pink)
padding: .5rem 1rem
border-radius: .8rem
color: white
.aspBox
width: 100%
img
width: 50%
max-width: 150px
border-radius: $loopShape
.aspProf
font-size: .8rem !important
margin-top: 1rem
width: 80%
color: darken($primaryColor, 20%) !important
b
font-family: 'Mainfont-Bold'
color: $darkgrey
margin-left: .2rem
font-size: .9rem
// Form-group Anpassungen
.form-group
position: relative
margin-bottom: .8rem
label
position: absolute
top: -.2rem
left: 0.5rem
font-size: 0.9rem
color: $primaryColor
pointer-events: none
transition: all 0.3s ease-in-out
input, textarea
padding-top: 1rem // Platz für das Label schaffen
input:focus + label, textarea:focus + label
top: -1rem
font-size: 0.8rem
color: $primaryColor
.error
color: #CC0000
font-size: .8rem
display: block
line-height: 1rem
</style>