ssr improve faq and marquee
This commit is contained in:
parent
fb73b7d2b5
commit
07851846ce
@ -1,219 +1,110 @@
|
||||
<template>
|
||||
<div class="myAccordion">
|
||||
<div v-for="(item, index) in items" :key="index" class="accordion-item">
|
||||
<div
|
||||
class="accordion-header"
|
||||
:ref="el => setHeaderRef(el, index)"
|
||||
@click="toggleSection(index)"
|
||||
>
|
||||
<div class="accordion-title">{{ item.title }}</div>
|
||||
<div class="accordion-toggle" :class="{ open: openIndex === index }">
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="myAccordion">
|
||||
<article
|
||||
v-for="(item, i) in items"
|
||||
:key="i"
|
||||
class="accordion-item"
|
||||
>
|
||||
<!-- Header --------------------------------------------------->
|
||||
<button
|
||||
class="accordion-header"
|
||||
:aria-expanded="openIndex === i"
|
||||
@click="toggle(i)"
|
||||
>
|
||||
<span class="accordion-title">{{ item.title }}</span>
|
||||
|
||||
<!-- Toggle-Icon (2 Striche) -->
|
||||
<span class="accordion-toggle" :class="{ open: openIndex === i }">
|
||||
<span></span><span></span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- Content --------------------------------------------------->
|
||||
<transition name="accordion">
|
||||
<div
|
||||
v-if="openIndex === i"
|
||||
class="accordion-content"
|
||||
:ref="el => setContentRef(el, index)"
|
||||
:style="{ maxHeight: openIndex === index ? `${contentHeights[index]}px` : '0px' }"
|
||||
>
|
||||
<p><span v-html="htmlContent(item.content)"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, nextTick } from 'vue';
|
||||
import { useHtmlConverter } from '../composables/useHTMLConverter';
|
||||
|
||||
const { convertToHTML } = useHtmlConverter();
|
||||
const htmlContent = (data) => {
|
||||
return convertToHTML(data); // Nutze die convertToHTML Funktion der Composable
|
||||
};
|
||||
|
||||
// Props für die Items
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
// Zustand für das geöffnete Element
|
||||
const openIndex = ref(null);
|
||||
// Referenzen für die Inhalte
|
||||
const contentRefs = ref([]);
|
||||
// Höhen der Inhalte
|
||||
const contentHeights = ref([]);
|
||||
// Referenzen für die Header-Elemente
|
||||
const headerRefs = ref([]);
|
||||
|
||||
// Funktion zum Umschalten des geöffneten Abschnitts
|
||||
const toggleSection = async (index) => {
|
||||
// Umschalten des offenen Indexes
|
||||
openIndex.value = openIndex.value === index ? null : index;
|
||||
|
||||
// Wenn ein neuer Abschnitt geöffnet wurde
|
||||
if (openIndex.value !== null) {
|
||||
await nextTick(); // Warten, bis das DOM aktualisiert wurde
|
||||
|
||||
setTimeout(() => {
|
||||
const header = headerRefs.value[openIndex.value]; // Header des offenen Elements
|
||||
const fixedHeaderHeight = document.querySelector('.fixed-header')?.offsetHeight || 0;
|
||||
|
||||
if (header) {
|
||||
const topPosition = header.getBoundingClientRect().top + window.scrollY - fixedHeaderHeight - 20; // 20px Puffer
|
||||
|
||||
// Scrollen zur berechneten Position
|
||||
window.scrollTo({
|
||||
top: topPosition > 0 ? topPosition : 0, // Verhindert negatives Scrollen
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}, 100); // Kleiner Timeout für das reibungslose Scrollen
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
// Setze die Referenzen dynamisch
|
||||
const setContentRef = (el, index) => {
|
||||
if (el) {
|
||||
contentRefs.value[index] = el;
|
||||
}
|
||||
};
|
||||
|
||||
const setHeaderRef = (el, index) => {
|
||||
if (el) {
|
||||
headerRefs.value[index] = el;
|
||||
}
|
||||
};
|
||||
|
||||
// Berechne die Höhen der Inhalte
|
||||
const calculateHeights = () => {
|
||||
contentHeights.value = contentRefs.value.map((el) => {
|
||||
if (!el) return 0;
|
||||
const additionalHeight = 50; // Fester Wert für oben und unten
|
||||
return el.scrollHeight + additionalHeight; // Dynamische Höhe + fester Wert
|
||||
});
|
||||
};
|
||||
|
||||
// Initialisiere die Berechnung nach dem Mounten
|
||||
onMounted(async () => {
|
||||
contentRefs.value = Array.from({ length: props.items.length });
|
||||
headerRefs.value = Array.from({ length: props.items.length });
|
||||
await nextTick();
|
||||
calculateHeights();
|
||||
});
|
||||
|
||||
// Aktualisiere die Höhen, wenn sich die Items ändern
|
||||
watch(
|
||||
() => props.items,
|
||||
async () => {
|
||||
contentRefs.value = Array.from({ length: props.items.length });
|
||||
headerRefs.value = Array.from({ length: props.items.length });
|
||||
await nextTick();
|
||||
calculateHeights();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.myAccordion {
|
||||
|
||||
.accordion-item {
|
||||
border-bottom: 1px solid #ccc;
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.accordion-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 1rem;
|
||||
font-family: 'Mainfont-Bold';
|
||||
background-color: white;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten(#EEEBE5, 6%);
|
||||
}
|
||||
|
||||
.accordion-title {
|
||||
flex: 1; // Nimmt den freien Platz ein
|
||||
text-align: left; // Links ausgerichtet
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.accordion-toggle {
|
||||
position: relative;
|
||||
flex-shrink: 0; // Verhindert, dass das Icon skaliert wird
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
width: 15px; // Kürzere Linien für saubere Spitzen
|
||||
height: 2px;
|
||||
background-color: #333;
|
||||
transition: transform 0.3s ease, left 0.3s ease, top 0.3s ease;
|
||||
top: 8px;
|
||||
|
||||
&:first-child {
|
||||
transform: rotate(135deg); // Linie 1: Oben links zur Mitte
|
||||
left: 5px; // Leicht nach links verschoben
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
transform: rotate(-135deg); // Linie 2: Oben rechts zur Mitte
|
||||
left: -5px; // Leicht nach rechts verschoben
|
||||
}
|
||||
}
|
||||
|
||||
&.open {
|
||||
span:first-child {
|
||||
transform: rotate(45deg); // Linie 1: Teil des "X"
|
||||
left: 0; // Zentriert
|
||||
}
|
||||
|
||||
span:last-child {
|
||||
transform: rotate(-45deg); // Linie 2: Teil des "X"
|
||||
left: 0; // Zentriert
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-content {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease-out;
|
||||
padding: 0 1rem;
|
||||
background-color: white;
|
||||
h3 {
|
||||
font-size: 1rem;
|
||||
line-height: 1.4rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
p {
|
||||
margin: 1rem 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
v-html="item.html"
|
||||
/>
|
||||
</transition>
|
||||
</article>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
defineProps<{ items: { title: string; html: string }[] }>()
|
||||
const openIndex = ref<number | null>(null)
|
||||
const toggle = (i: number) =>
|
||||
(openIndex.value = openIndex.value === i ? null : i)
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.myAccordion
|
||||
.accordion-item
|
||||
border-bottom: 1px solid $lightgrey
|
||||
width: 100%
|
||||
|
||||
.accordion-header
|
||||
all: unset
|
||||
width: 100%
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
align-items: center
|
||||
padding: 1.2rem 1rem 1.2rem 1rem
|
||||
background: #fff
|
||||
cursor: pointer
|
||||
border: 0
|
||||
font: inherit
|
||||
text-align: left
|
||||
font-family: 'Mainfont-Bold'
|
||||
|
||||
&:hover
|
||||
background: linear-gradient(to left, white 1%, lighten($lightgrey, 2%) 80%, white 100%)
|
||||
|
||||
// Icon
|
||||
.accordion-toggle
|
||||
position: relative
|
||||
width: 20px
|
||||
height: 20px
|
||||
flex-shrink: 0
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
span
|
||||
position: absolute
|
||||
width: 16px
|
||||
height: 2px
|
||||
background: #333
|
||||
transition: transform .25s
|
||||
|
||||
&:first-child
|
||||
transform: rotate(90deg) // horizontale Linie wird senkrecht
|
||||
|
||||
|
||||
&.open
|
||||
span:first-child
|
||||
transform: rotate(45deg)
|
||||
span:last-child
|
||||
transform: rotate(-45deg)
|
||||
|
||||
// Content
|
||||
.accordion-content
|
||||
padding: 0 1rem 1rem
|
||||
|
||||
// simple height-fade transition
|
||||
.accordion-enter-from,
|
||||
.accordion-leave-to
|
||||
max-height: 0
|
||||
opacity: 0
|
||||
|
||||
.accordion-enter-active,
|
||||
.accordion-leave-active
|
||||
transition: all .25s ease
|
||||
|
||||
.accordion-enter-to,
|
||||
.accordion-leave-from
|
||||
max-height: 500px // ausreichend groß
|
||||
opacity: 1
|
||||
</style>
|
||||
|
||||
@ -24,7 +24,14 @@
|
||||
>
|
||||
<div class="row left m-2">
|
||||
<div class="col-md-6" id="hintBox">
|
||||
<img class="mobileAspBox" v-if="screenWidth <= 768" :src="cmsUrl + companyinfo?.profileImage?.data?.attributes?.url" alt="Ansprechpartner Sabrina Hennrich">
|
||||
<NuxtImg
|
||||
v-if="screenWidth <= 768"
|
||||
class="mobileAspBox"
|
||||
:src="cmsUrl + companyinfo?.profileImage?.data?.attributes?.url"
|
||||
alt="Sabrina Hennrich"
|
||||
:width="400"
|
||||
format="webp"
|
||||
/>
|
||||
<h2 id="contactTitle">{{ $t('contactForm.yourcontact2us') }}</h2>
|
||||
<p class="my-4">
|
||||
<svg aria-hidden="true">
|
||||
@ -38,8 +45,12 @@
|
||||
{{ 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"><img :src="cmsUrl + companyinfo?.profileImage?.data?.attributes?.url" alt="Ansprechpartner Sabrina Hennrich"></div>
|
||||
|
||||
<div class="aspBox"><NuxtImg
|
||||
:src="cmsUrl + companyinfo?.profileImage?.data?.attributes?.url"
|
||||
alt="Ansprechpartner Sabrina Hennrich"
|
||||
:width="400"
|
||||
format="webp"
|
||||
/></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@ -99,7 +110,7 @@
|
||||
<span class="check">✔</span>
|
||||
{{ $t('contactForm.privacyInfotextBeforeLink') }}
|
||||
<NuxtLinkLocale
|
||||
to="/privacy"
|
||||
:to="$t('privacy')"
|
||||
:aria-label="$t('privacy')"
|
||||
>
|
||||
{{ $t('contactForm.privacyInfotextLinkText') }}
|
||||
|
||||
@ -1,50 +1,46 @@
|
||||
<template>
|
||||
<section class="faq" id="faq">
|
||||
<h3>{{ headline }}</h3>
|
||||
<Accordion v-if="accordionItems.length" :items="accordionItems" />
|
||||
<p v-else>Lade Daten...</p>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-6 mb-3">
|
||||
<h4> Noch Fragen? </h4>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<button @click.prevent="toggleContactBubble" role="button" class="pinkBtn">
|
||||
{{ button }}
|
||||
</button>
|
||||
</div>
|
||||
<section class="faq" id="faq">
|
||||
<h3>{{ headline }}</h3>
|
||||
<Accordion v-if="accordionItems.length" :items="accordionItems" />
|
||||
<p v-else>Lade Daten...</p>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-6 mb-3">
|
||||
<h4> Noch Fragen? </h4>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useMainStore } from '@/stores/main';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, defineProps, defineAsyncComponent } from 'vue';
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
pageLink: { type: String, required: true },
|
||||
headline: { type: String, default: "Häufig gestellte Fragen (FAQs)" },
|
||||
button: { type: String, default: "Sprechen Sie uns gerne an!" },
|
||||
});
|
||||
|
||||
const mainStore = useMainStore();
|
||||
const { pages } = storeToRefs(mainStore); // Wir holen die `pages` aus dem Pinia-Store
|
||||
|
||||
const toggleContactBubble = () => mainStore.toggleContactBubble();
|
||||
|
||||
// 🔹 **FAQs für die aktuelle Seite aus `pages` filtern**
|
||||
const accordionItems = computed(() => {
|
||||
const currentPage = pages.value?.find(page => page.pageLink === props.pageLink);
|
||||
const faqsArray = Array.isArray(currentPage?.faqs.data) ? currentPage.faqs.data : []; // Sicherstellen, dass es ein Array ist
|
||||
return faqsArray.map(faq => ({
|
||||
title: faq.attributes.question,
|
||||
content: faq.attributes.answer,
|
||||
}));
|
||||
});
|
||||
|
||||
</script>
|
||||
<div class="col-md-6">
|
||||
<button @click.prevent="toggleContactBubble" role="button" class="pinkBtn">
|
||||
{{ button }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineProps } from 'vue'
|
||||
import { useMainStore } from '@/stores/main'
|
||||
import { useHtmlConverter } from '~/composables/useHTMLConverter'
|
||||
const { convertToHTML } = useHtmlConverter()
|
||||
|
||||
const props = defineProps({
|
||||
pageLink: { type: String, required: true },
|
||||
headline: { type: String, default: "Häufig gestellte Fragen (FAQs)" },
|
||||
button: { type: String, default: "Sprechen Sie uns gerne an!" },
|
||||
})
|
||||
|
||||
const mainStore = useMainStore()
|
||||
|
||||
const accordionItems = computed(() =>
|
||||
mainStore.getFaqsByPageLink(props.pageLink).map(faq => ({
|
||||
title: faq.question,
|
||||
html: convertToHTML(faq.answer) // <- hier passiert die Umwandlung
|
||||
}))
|
||||
)
|
||||
|
||||
const toggleContactBubble = () => mainStore.toggleContactBubble()
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="sass">
|
||||
.faq
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
id="wave"
|
||||
d=" M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z "
|
||||
fill="#EEEBE5"
|
||||
></path>
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
@ -21,52 +21,48 @@
|
||||
<div class="container">
|
||||
<h2 class="pt-4 pb-3">{{ title }}</h2>
|
||||
|
||||
<!-- Marquee mit doppeltem Inhalt für endloses Scrollen -->
|
||||
<div class="marquee marquee--hover-pause mt-5">
|
||||
<ul class="marquee__content">
|
||||
<li v-for="(item, index) in items" :key="index">
|
||||
<NuxtLink
|
||||
v-if="item.link"
|
||||
:to="`/${link}/${item.link}`"
|
||||
class="custLogoLink"
|
||||
>
|
||||
<img
|
||||
:src="cmsUrl + getImageUrl(item)"
|
||||
<NuxtLink v-if="item.link" :to="item.link" class="custLogoLink">
|
||||
<NuxtImg
|
||||
:src="cmsUrl + item.logo.url"
|
||||
alt="item.logo.alternativeText || 'Logo'"
|
||||
width="250"
|
||||
format="webp"
|
||||
class="custLogo"
|
||||
:style="{ height: logoHeight + 'px' }"
|
||||
:alt="getAltText(item)"
|
||||
/>
|
||||
</NuxtLink>
|
||||
<img
|
||||
<NuxtImg
|
||||
v-else
|
||||
:src="cmsUrl + getImageUrl(item)"
|
||||
:src="cmsUrl + item.logo.url"
|
||||
alt="item.logo.alternativeText || 'Logo'"
|
||||
width="250"
|
||||
format="webp"
|
||||
class="custLogo"
|
||||
:style="{ height: logoHeight + 'px' }"
|
||||
:alt="getAltText(item)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul aria-hidden="true" class="marquee__content">
|
||||
<li v-for="(item, index) in items" :key="'duplicate-' + index">
|
||||
<NuxtLink
|
||||
v-if="item.link"
|
||||
:to="`/${link}/${item.link}`"
|
||||
class="custLogoLink"
|
||||
>
|
||||
<img
|
||||
:src="cmsUrl + getImageUrl(item)"
|
||||
<!-- Duplizierte Liste für Endlos-Scroll -->
|
||||
<ul class="marquee__content duplicate" aria-hidden="true">
|
||||
<li v-for="(item, index) in items" :key="'dup-'+index">
|
||||
<NuxtLink v-if="item.link" :to="item.link" class="custLogoLink">
|
||||
<NuxtImg
|
||||
:src="cmsUrl + item.logo.url"
|
||||
alt="item.logo.alternativeText || 'Logo'"
|
||||
width="250"
|
||||
format="webp"
|
||||
class="custLogo"
|
||||
:style="{ height: logoHeight + 'px' }"
|
||||
:alt="getAltText(item)"
|
||||
/>
|
||||
</NuxtLink>
|
||||
<img
|
||||
<NuxtImg
|
||||
v-else
|
||||
:src="cmsUrl + getImageUrl(item)"
|
||||
:src="cmsUrl + item.logo.url"
|
||||
alt="item.logo.alternativeText || 'Logo'"
|
||||
width="250"
|
||||
format="webp"
|
||||
class="custLogo"
|
||||
:style="{ height: logoHeight + 'px' }"
|
||||
:alt="getAltText(item)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
@ -105,10 +101,13 @@
|
||||
|
||||
|
||||
<script setup>
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
const cmsUrl = computed(() => runtimeConfig.public.cmsBaseUrl)
|
||||
import { computed } from 'vue'
|
||||
|
||||
// Props: title, items, logoHeight, und link (optional)
|
||||
// Runtime config & base CMS URL
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const cmsUrl = computed(() => runtimeConfig.public.cmsBaseUrl)
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
@ -116,7 +115,7 @@
|
||||
},
|
||||
logoHeight: {
|
||||
type: Number,
|
||||
default: 50, // Standardhöhe in Pixel
|
||||
default: 50,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
@ -124,32 +123,9 @@
|
||||
},
|
||||
link: {
|
||||
type: String,
|
||||
default: 'projekt', // Standardwert, wenn keine spezifische Seite angegeben wird
|
||||
default: 'projekt',
|
||||
},
|
||||
});
|
||||
|
||||
// Funktion zur Bestimmung des Bild-URLs basierend auf Typ des Items
|
||||
const getImageUrl = (item) => {
|
||||
if (item.logo) {
|
||||
// Für Customers: Verwende das logo-Feld
|
||||
return item.logo.data.attributes.url;
|
||||
} else if (item.projectImages && item.projectImages.data.length > 0) {
|
||||
// Für Projects: Verwende das erste Bild im projectImages-Feld
|
||||
return item.projectImages.data[0].attributes.url;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// Funktion zur Ermittlung des Alt-Texts für das Bild
|
||||
const getAltText = (item) => {
|
||||
if (item.logo) {
|
||||
return item.logo.data.attributes.alternativeText || item.company || '';
|
||||
} else if (item.projectImages && item.projectImages.data.length > 0) {
|
||||
return item.projectImages.data[0].attributes.alternativeText || item.projectTitle || '';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
|
||||
@ -1,43 +1,39 @@
|
||||
// composables/usePageMeta.ts
|
||||
import { watch, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useMainStore } from '~/stores/main'
|
||||
import { useMainStore } from '@/stores/main'
|
||||
import { useHead } from '@unhead/vue'
|
||||
|
||||
export function usePageMeta() {
|
||||
export function usePageMeta () {
|
||||
const route = useRoute()
|
||||
const pageLink = route.path
|
||||
const mainStore = useMainStore()
|
||||
|
||||
const page = mainStore.getPageByLink(pageLink)
|
||||
console.log(page)
|
||||
// ► Der aktuelle Page-Eintrag als computed
|
||||
const currentPage = computed(() => mainStore.getPageByLink(route.path))
|
||||
|
||||
if (!page) {
|
||||
console.warn(`Keine Seite gefunden für den pageLink "${pageLink}"`)
|
||||
return
|
||||
}
|
||||
// ► Reagiere auf Route- oder Store-Änderungen
|
||||
watch(
|
||||
() => currentPage.value, // Quelle
|
||||
(page) => { // Callback
|
||||
if (!page) return
|
||||
|
||||
const metaTitle = page.SEO?.pageTitle || 'Standard Title'
|
||||
const metaDescription = page.SEO?.seoDescription || 'Standard Description'
|
||||
const metaImage = page.SEO?.seoImage?.url || '/default-image.jpg'
|
||||
const metaTitle = page.SEO?.pageTitle ?? 'Standard Title'
|
||||
const metaDescription = page.SEO?.seoDescription ?? 'Standard Description'
|
||||
const metaImage = page.SEO?.seoImage?.url ?? '/default-image.jpg'
|
||||
|
||||
try {
|
||||
JSON.stringify(metaTitle)
|
||||
JSON.stringify(metaDescription)
|
||||
JSON.stringify(metaImage)
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Serialisieren der Meta-Daten:', err)
|
||||
}
|
||||
|
||||
useHead({
|
||||
title: metaTitle,
|
||||
meta: [
|
||||
{ name: 'description', content: metaDescription },
|
||||
{ property: 'og:title', content: metaTitle },
|
||||
{ property: 'og:description', content: metaDescription },
|
||||
{ property: 'og:image', content: metaImage },
|
||||
{ name: 'twitter:title', content: metaTitle },
|
||||
{ name: 'twitter:description', content: metaDescription },
|
||||
{ name: 'twitter:image', content: metaImage },
|
||||
]
|
||||
})
|
||||
useHead({
|
||||
title: metaTitle,
|
||||
meta: [
|
||||
{ name: 'description', content: metaDescription },
|
||||
{ property: 'og:title', content: metaTitle },
|
||||
{ property: 'og:description', content: metaDescription },
|
||||
{ property: 'og:image', content: metaImage },
|
||||
{ name: 'twitter:title', content: metaTitle },
|
||||
{ name: 'twitter:description', content: metaDescription },
|
||||
{ name: 'twitter:image', content: metaImage }
|
||||
]
|
||||
})
|
||||
},
|
||||
{ immediate: true } // Sofort beim ersten Aufruf ausführen
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="homePage">
|
||||
<section class="heroBox">
|
||||
<section class="heroBox" aria-labelledby="hero-heading">
|
||||
<div class="container-10">
|
||||
<h1>{{ $t('home.heroBox.h1') }}</h1>
|
||||
<h1 id="hero-heading">{{ $t('home.heroBox.h1') }}</h1>
|
||||
<h2>{{ $t('home.heroBox.h2') }}</h2>
|
||||
<h3>{{ $t('home.heroBox.h3') }}</h3>
|
||||
</div>
|
||||
@ -11,21 +11,28 @@
|
||||
<path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#FFF"></path>
|
||||
</svg>
|
||||
</section>
|
||||
<section>
|
||||
<section aria-labelledby="solution-title">
|
||||
<div class="container-10 webStrategy">
|
||||
<img class="imgFloatLeft" src="https://strapi.digimedialoop.de/uploads/web_wireframe_Strategie_0bae802a68.png" alt="wireframe web strategie">
|
||||
<NuxtImg
|
||||
class="imgFloatLeft"
|
||||
src="https://strapi.digimedialoop.de/uploads/web_wireframe_Strategie_0bae802a68.png"
|
||||
alt="Wireframe Web Strategie"
|
||||
width="300"
|
||||
format="webp"
|
||||
/>
|
||||
|
||||
<h2>{{ $t('home.solution.title') }}</h2>
|
||||
<h2 id="solution-title">{{ $t('home.solution.title') }}</h2>
|
||||
<h3>{{ $t('home.solution.teaser') }}</h3>
|
||||
<p>{{ $t('home.solution.text') }}</p>
|
||||
<button class="mintBtn"
|
||||
role="button"
|
||||
aria-describedby="solution-title"
|
||||
aria-label="headless CMS Info" @click="navigateToArticle">{{ $t('home.solution.buttonText') }}</button>
|
||||
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<section class="targetGroup">
|
||||
<section class="targetGroup" aria-labelledby="invitation-title">
|
||||
<svg class="sectionWave wave-top" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 20">
|
||||
<path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#FFF"></path>
|
||||
</svg>
|
||||
@ -34,10 +41,11 @@
|
||||
<div class="col-md-4">
|
||||
</div>
|
||||
<div class="col-md-8 pt-5 pb-5">
|
||||
<h2>{{ $t('home.invitation.title') }}</h2>
|
||||
<h2 id="invitation-title">{{ $t('home.invitation.title') }}</h2>
|
||||
<h3>{{ $t('home.invitation.teaser') }}</h3>
|
||||
<button class="pinkBtn" @click.prevent="toggleContactBubble"
|
||||
role="button"
|
||||
aria-describedby="invitation-title"
|
||||
aria-label="Kontaktformular öffnen">{{ $t('home.invitation.button') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -76,9 +84,9 @@
|
||||
<div class="row">
|
||||
<div class="col-md-6 my-5">
|
||||
<div class="innerBox">
|
||||
<div class="canDoItem">
|
||||
<div class="canDoItem" role="group" aria-labelledby="cando-title">
|
||||
<div class="imageBox" style="background-image: url('https://strapi.digimedialoop.de/uploads/Screen_Shot_Tool_20250228133408_beb2a11980.png');"></div>
|
||||
<h3>{{ $t('home.canDo.item3.title') }}</h3>
|
||||
<h3 id="cando-title">{{ $t('home.canDo.item3.title') }}</h3>
|
||||
<p>{{ $t('home.canDo.item3.text') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -130,7 +138,7 @@
|
||||
:aria-label="$t('home.finalCall.button')">{{ $t('home.finalCall.button') }}</button>
|
||||
</div>
|
||||
</section>
|
||||
<MarqueeBanner :items="customers" :logoHeight="60" :title="$t('home.marqueeBanner.title')" />
|
||||
<MarqueeBanner :items="logoItems" :logoHeight="60" :title="$t('home.marqueeBanner.title')" />
|
||||
<FAQArea pageLink="/" :headline="$t('home.faqArea.headline')" />
|
||||
|
||||
</div>
|
||||
@ -144,8 +152,11 @@ import { defineAsyncComponent } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
const cmsUrl = computed(() => runtimeConfig.public.cmsBaseUrl)
|
||||
|
||||
const mainStore = useMainStore();
|
||||
const { cmsUrl, customers } = storeToRefs(mainStore);
|
||||
const { customers } = storeToRefs(mainStore);
|
||||
const toggleContactBubble = () => mainStore.toggleContactBubble();
|
||||
|
||||
const navigateToArticle = () => {
|
||||
@ -155,6 +166,16 @@ const navigateToArticle = () => {
|
||||
const screenWidth = computed(() => mainStore.screenWidth);
|
||||
const waveHeight = computed(() => (screenWidth.value / 25).toFixed(0));
|
||||
|
||||
const logoItems = computed(() => {
|
||||
return customers.value.map(customer => ({
|
||||
company: customer.company || '',
|
||||
logo: {
|
||||
url: customer.logo?.url || '',
|
||||
alternativeText: customer.company || ''
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
import { useMainStore } from '@/stores/main'
|
||||
|
||||
export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
const store = useMainStore()
|
||||
await store.fetchInitialData()
|
||||
})
|
||||
const mainStore = useMainStore(nuxtApp.$pinia)
|
||||
if (!mainStore.dataFetched) {
|
||||
await mainStore.fetchInitialData()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
240
stores/main.ts
240
stores/main.ts
@ -24,36 +24,53 @@ interface CompanyInfo {
|
||||
}
|
||||
}
|
||||
|
||||
interface SEO {
|
||||
pageTitle: string
|
||||
seoDescription: string // Achtung: Schreibfehler wird so übernommen
|
||||
seoKeywords: string
|
||||
type: string
|
||||
seoImage?: CompanyLogo | null
|
||||
}
|
||||
|
||||
interface PageSection {
|
||||
id: number
|
||||
sectionText: string
|
||||
sectionImage?: CompanyLogo | null
|
||||
}
|
||||
|
||||
interface FAQ {
|
||||
question: string
|
||||
answer: string
|
||||
}
|
||||
|
||||
interface Page {
|
||||
id: number
|
||||
pageName: string
|
||||
pageLink: string
|
||||
header_image?: {
|
||||
url: string
|
||||
alternativeText?: string
|
||||
} | null
|
||||
SEO?: {
|
||||
pageTitle: string
|
||||
seoDescription: string
|
||||
seoKeywords: string
|
||||
type: string
|
||||
seoImage?: {
|
||||
url: string
|
||||
alternativeText?: string
|
||||
} | null
|
||||
} | null
|
||||
faqs: Array<{
|
||||
question: string
|
||||
answer: string
|
||||
}>
|
||||
pageSections: Array<{
|
||||
id: number
|
||||
sectionText: string
|
||||
sectionImage?: {
|
||||
url: string
|
||||
alternativeText?: string
|
||||
} | null
|
||||
}>
|
||||
header_image?: CompanyLogo | null
|
||||
SEO?: SEO | null
|
||||
faqs: FAQ[]
|
||||
pageSections: PageSection[]
|
||||
}
|
||||
|
||||
interface CustomerProject {
|
||||
id: number
|
||||
projectTitle: string
|
||||
launchDate?: string
|
||||
projectDescription?: string
|
||||
link?: string
|
||||
webpage?: string
|
||||
technologies: { titel: string; icon?: string }[]
|
||||
projectImages: CompanyLogo[]
|
||||
}
|
||||
|
||||
interface Customer {
|
||||
id: number
|
||||
company: string
|
||||
city: string
|
||||
logo?: CompanyLogo | null
|
||||
invertLogo?: CompanyLogo | null
|
||||
projects: CustomerProject[]
|
||||
}
|
||||
|
||||
export const useMainStore = defineStore('main', {
|
||||
@ -64,108 +81,163 @@ export const useMainStore = defineStore('main', {
|
||||
screenWidth: 1440,
|
||||
companyinfo: null as CompanyInfo | null,
|
||||
pages: [] as Page[],
|
||||
customers: [] as Customer[],
|
||||
dataFetched: false,
|
||||
loading: false,
|
||||
error: null as { message: string, stack?: string } | null,
|
||||
error: null as { message: string; stack?: string } | null,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
invertLogoUrl: (state) => {
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const logoUrl = state.companyinfo?.invertlogo?.data?.attributes?.url
|
||||
return logoUrl
|
||||
return logoUrl
|
||||
? `${runtimeConfig.public.cmsBaseUrl}${logoUrl}`
|
||||
: '/uploads/dummy_Image_4abc3f04dd.webp'
|
||||
},
|
||||
|
||||
isMobile: (state) => state.screenWidth < 768,
|
||||
|
||||
/** Neuer Getter: Seite anhand pageLink finden */
|
||||
getPageByLink: (state) => {
|
||||
return (link: string) => state.pages.find(p => p.pageLink === link)
|
||||
getPageByLink: (state) => (link: string) =>
|
||||
state.pages.find((p) => p.pageLink === link),
|
||||
|
||||
getCustomerById: (state) => (id: number) =>
|
||||
state.customers.find((c) => c.id === id),
|
||||
|
||||
getFaqsByPageId: (state) => (pageId: number) =>
|
||||
state.pages.find((p) => p.id === pageId)?.faqs ?? [],
|
||||
|
||||
getFaqsByPageLink: (state) => (link: string) => {
|
||||
const page = state.pages.find(p => p.pageLink === link);
|
||||
return page?.faqs ?? [];
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
|
||||
actions: {
|
||||
toggleMenu() {
|
||||
this.menuOpen = !this.menuOpen
|
||||
},
|
||||
|
||||
|
||||
closeMenu() {
|
||||
this.menuOpen = false
|
||||
},
|
||||
|
||||
|
||||
toggleContactBubble() {
|
||||
this.contactBoxOpen = !this.contactBoxOpen
|
||||
},
|
||||
|
||||
|
||||
setScrollPosition(pos: number) {
|
||||
this.scrollPosition = pos
|
||||
},
|
||||
|
||||
|
||||
setScreenWidth(width: number) {
|
||||
this.screenWidth = width
|
||||
},
|
||||
|
||||
|
||||
async fetchInitialData() {
|
||||
if (this.dataFetched) return
|
||||
|
||||
|
||||
this.loading = true
|
||||
const { public: cfg } = useRuntimeConfig()
|
||||
|
||||
try {
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const cmsUrl = runtimeConfig.public.cmsBaseUrl
|
||||
const [companyRes, pagesRes, customersRes] = await Promise.all([
|
||||
$fetch(`${cfg.cmsBaseUrl}/api/companyinfo?populate=*`, {
|
||||
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
|
||||
}),
|
||||
$fetch(`${cfg.cmsBaseUrl}/api/pages?populate=*`, {
|
||||
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
|
||||
}),
|
||||
$fetch(`${cfg.cmsBaseUrl}/api/customers?populate=*`, {
|
||||
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
|
||||
}),
|
||||
])
|
||||
|
||||
const companyRes = await $fetch(`${cmsUrl}/api/companyinfo?populate=*`, {
|
||||
headers: { 'Authorization': `Bearer ${runtimeConfig.public.cmsToken}` }
|
||||
})
|
||||
this.companyinfo = companyRes.data?.attributes || companyRes
|
||||
// CompanyInfo (Single Type)
|
||||
this.companyinfo = companyRes.data?.attributes ?? companyRes
|
||||
|
||||
const pagesRes = await $fetch(`${cmsUrl}/api/pages?populate=*`, {
|
||||
headers: { 'Authorization': `Bearer ${runtimeConfig.public.cmsToken}` }
|
||||
// Pages
|
||||
this.pages = pagesRes.data.map((item: any) => {
|
||||
const a = item.attributes
|
||||
return {
|
||||
id: item.id,
|
||||
pageName: a.pageName,
|
||||
pageLink: a.pageLink,
|
||||
header_image: a.header_image?.data
|
||||
? {
|
||||
url: a.header_image.data.attributes.url,
|
||||
alternativeText: a.header_image.data.attributes.alternativeText,
|
||||
}
|
||||
: null,
|
||||
SEO: a.SEO
|
||||
? {
|
||||
pageTitle: a.SEO.pageTitle,
|
||||
seoDescription: a.SEO.seoDesicription, // Fehler absichtlich
|
||||
seoKeywords: a.SEO.seoKeywords,
|
||||
type: a.SEO.type,
|
||||
seoImage: a.SEO.seoImage?.data
|
||||
? {
|
||||
url: a.SEO.seoImage.data.attributes.url,
|
||||
alternativeText: a.SEO.seoImage.data.attributes.alternativeText,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
: null,
|
||||
faqs: a.faqs?.data?.map((f: any) => ({
|
||||
question: f.attributes.question,
|
||||
answer: f.attributes.answer,
|
||||
})) ?? [],
|
||||
pageSections: a.pageSections?.map((s: any) => ({
|
||||
id: s.id,
|
||||
sectionText: s.sectionText,
|
||||
sectionImage: s.sectionImage?.data
|
||||
? {
|
||||
url: s.sectionImage.data.attributes.url,
|
||||
alternativeText: s.sectionImage.data.attributes.alternativeText,
|
||||
}
|
||||
: null,
|
||||
})) ?? [],
|
||||
}
|
||||
})
|
||||
|
||||
this.pages = pagesRes.data.map((item: any) => ({
|
||||
id: item.id,
|
||||
pageName: item.attributes.pageName,
|
||||
pageLink: item.attributes.pageLink,
|
||||
header_image: item.attributes.header_image ? {
|
||||
url: item.attributes.header_image.data.attributes.url,
|
||||
alternativeText: item.attributes.header_image.data.attributes.alternativeText
|
||||
} : null,
|
||||
SEO: item.attributes.SEO ? {
|
||||
pageTitle: item.attributes.SEO.pageTitle,
|
||||
seoDescription: item.attributes.SEO.seoDesicription,
|
||||
seoKeywords: item.attributes.SEO.seoKeywords,
|
||||
type: item.attributes.SEO.type,
|
||||
seoImage: item.attributes.SEO.seoImage ? {
|
||||
url: item.attributes.SEO.seoImage.data.attributes.url,
|
||||
alternativeText: item.attributes.SEO.seoImage.data.attributes.alternativeText
|
||||
} : null
|
||||
} : null,
|
||||
faqs: item.attributes.faqs ? item.attributes.faqs.data.map((faq: any) => ({
|
||||
question: faq.attributes.question,
|
||||
answer: faq.attributes.answer
|
||||
})) : [],
|
||||
pageSections: item.attributes.pageSections ? item.attributes.pageSections.map((section: any) => ({
|
||||
id: section.id,
|
||||
sectionText: section.sectionText,
|
||||
sectionImage: section.sectionImage ? {
|
||||
url: section.sectionImage.data.attributes.url,
|
||||
alternativeText: section.sectionImage.data.attributes.alternativeText
|
||||
} : null
|
||||
})) : []
|
||||
}))
|
||||
// Customers
|
||||
this.customers = customersRes.data.map((item: any) => {
|
||||
const a = item.attributes
|
||||
return {
|
||||
id: item.id,
|
||||
company: a.company,
|
||||
city: a.city,
|
||||
logo: a.logo?.data?.attributes ?? null,
|
||||
invertLogo: a.invertLogo?.data?.attributes ?? null,
|
||||
projects: (a.projects?.data ?? []).map((p: any) => ({
|
||||
id: p.id,
|
||||
projectTitle: p.attributes.projectTitle,
|
||||
projectImages: p.attributes.projectImages?.data?.map((img: any) => ({
|
||||
url: img.attributes.url,
|
||||
alternativeText: img.attributes.alternativeText,
|
||||
})) ?? [],
|
||||
launchDate: p.attributes.launchDate,
|
||||
projectDescription: p.attributes.projectDescription,
|
||||
link: p.attributes.link,
|
||||
webpage: p.attributes.webpage,
|
||||
technologies: p.attributes.Technologien?.data?.map((t: any) => ({
|
||||
titel: t.attributes.titel,
|
||||
icon: t.attributes.icon,
|
||||
})) ?? [],
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
this.dataFetched = true
|
||||
} catch (err) {
|
||||
const errorObj = err as Error
|
||||
this.error = {
|
||||
message: errorObj.message,
|
||||
stack: errorObj.stack
|
||||
}
|
||||
console.error('Fehler beim Laden der Daten:', errorObj)
|
||||
const e = err as Error
|
||||
this.error = { message: e.message, stack: e.stack }
|
||||
console.error('Fetch-Fehler:', e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user