531 lines
15 KiB
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>
|
|
|